# 1 谈谈你对MVVM的理解
为什么要有这些模式,目的:职责划分、分层(将Model层、View层进行分类)借鉴后端思想,对于前端而已,就是如何将数据同步到页面上
MVC模式 代表:Backbone + underscore + jquery

- 传统的
MVC指的是,用户操作会请求服务端路由,路由会调用对应的控制器来处理,控制器会获取数据。将结果返回给前端,页面重新渲染 MVVM:传统的前端会将数据手动渲染到页面上,MVVM模式不需要用户收到操作dom元素,将数据绑定到viewModel层上,会自动将数据渲染到页面中,视图变化会通知viewModel层 更新数据。ViewModel就是我们MVVM模式中的桥梁
MVVM模式 映射关系的简化,隐藏了controller

MVVM是Model-View-ViewModel缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。
Model: 代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。我们可以把Model称为数据层,因为它仅仅关注数据本身,不关心任何行为View: 用户操作界面。当ViewModel对Model进行更新的时候,会通过数据绑定更新到ViewViewModel: 业务逻辑层,View需要什么数据,ViewModel要提供这个数据;View有某些操作,ViewModel就要响应这些操作,所以可以说它是Model for View.
总结: MVVM模式简化了界面与业务的依赖,解决了数据频繁更新。MVVM 在使用当中,利用双向绑定技术,使得 Model 变化时,ViewModel 会自动更新,而 ViewModel 变化时,View 也会自动变化。
我们以下通过一个 Vue 实例来说明 MVVM 的具体实现
<!-- View 层 --> <div id="app"> <p>{{message}}</p> <button v-on:click="showMessage()">Click me</button> </div>@程序员poetry: 代码已经复制到剪贴板
// ViewModel 层 var app = new Vue({ el: '#app', data: { // 用于描述视图状态 message: 'Hello Vue!', }, methods: { // 用于描述视图行为 showMessage(){ let vm = this; alert(vm.message); } }, created(){ let vm = this; // Ajax 获取 Model 层的数据 ajax({ url: '/your/server/data/api', success(res){ vm.message = res; } }); } })@程序员poetry: 代码已经复制到剪贴板
// Model 层 { "url": "/your/server/data/api", "res": { "success": true, "name": "test", "domain": "www.baidu.com" } }@程序员poetry: 代码已经复制到剪贴板
# 2 谈谈你对SPA单页面的理解
SPA( single-page application )仅在Web页面初始化时加载相应的HTML、JavaScript和CSS。一旦页面加载完成,SPA不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现HTML内容的变换,UI与用户的交互,避免页面的重新加载
优点:
- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
- 基于上面一点,
SPA相对对服务器压力小; - 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理
缺点:
- 初次加载耗时多:为实现单页
Web应用功能及显示效果,需要在加载页面的时候将JavaScript、CSS统一加载,部分页面按需加载; - 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
SEO难度较大:由于所有的内容都在一个页面中动态替换显示,所以在SEO上其有着天然的弱势
# 3 Vue2.x 响应式数据原理
整体思路是数据劫持+观察者模式
对象内部通过 defineReactive 方法,使用 Object.defineProperty 来劫持各个属性的 setter、getter(只会劫持已经存在的属性),数组则是通过重写数组7个方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)
Object.defineProperty基本使用
function observer(value) { // proxy reflect if (typeof value === 'object' && typeof value !== null) for (let key in value) { defineReactive(value, key, value[key]); } } function defineReactive(obj, key, value) { observer(value); Object.defineProperty(obj, key, { get() { // 收集对应的key 在哪个方法(组件)中被使用 return value; }, set(newValue) { if (newValue !== value) { observer(newValue); value = newValue; // 让key对应的方法(组件重新渲染)重新执行 } } }) } let obj1 = { school: { name: 'poetry', age: 20 } }; observer(obj1); console.log(obj1)@程序员poetry: 代码已经复制到剪贴板
源码分析

class Observer { // 观测值 constructor(value) { this.walk(value); } walk(data) { // 对象上的所有属性依次进行观测 let keys = Object.keys(data); for (let i = 0; i < keys.length; i++) { let key = keys[i]; let value = data[key]; defineReactive(data, key, value); } } } // Object.defineProperty数据劫持核心 兼容性在ie9以及以上 function defineReactive(data, key, value) { observe(value); // 递归关键 // --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止 // 思考?如果Vue数据嵌套层级过深 >>性能会受影响 Object.defineProperty(data, key, { get() { console.log("获取值"); //需要做依赖收集过程 这里代码没写出来 return value; }, set(newValue) { if (newValue === value) return; console.log("设置值"); //需要做派发更新过程 这里代码没写出来 value = newValue; }, }); } export function observe(value) { // 如果传过来的是对象或者数组 进行属性劫持 if ( Object.prototype.toString.call(value) === "[object Object]" || Array.isArray(value) ) { return new Observer(value); } }@程序员poetry: 代码已经复制到剪贴板
说一说你对vue响应式理解回答范例
- 所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制
MVVM框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理- 以
vue为例说明,通过数据响应式加上虚拟DOM和patch算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度 vue2中的数据响应式会根据数据类型来做不同处理,如果是对象则采用Object.defineProperty()的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是数组则通过覆盖数组对象原型的7个变更方法,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete这样特殊的api才能生效;对于es6中新产生的Map、Set这些数据结构不支持等问题- 为了解决这些问题,
vue3重新编写了这一部分的实现:利用ES6的Proxy代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊api,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了
# 4 Vue3.x 响应式数据原理
Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。
# proxy基本用法
// proxy默认只会代理第一层对象,只有取值再次是对象的时候再次代理,不是一上来就代理,提高性能。不像vue2.x递归遍历每个对象属性 let handler = { set(target, key, value) { return Reflect.set(target, key, value); }, get(target, key) { if (typeof target[key] == 'object' && target[key] !== null) { return new Proxy(target[key], handler); // 懒代理,只有取值再次是对象的时候再次代理,提高性能 } return Reflect.get(target, key); } } let obj = { school: { name: 'poetry', age: 20 } }; let proxy = new Proxy(obj, handler); // 返回对象的代理 proxy.school@程序员poetry: 代码已经复制到剪贴板
# 说说你对 proxy 的理解,Proxy 相比于 defineProperty 的优势
Object.defineProperty() 的问题主要有三个:
- 不能监听数组的变化:无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应
- 必须遍历对象的每个属性:只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果属性值是对象,还需要深度遍历。
Proxy可以劫持整个对象,并返回一个新的对象 - 必须深层遍历嵌套的对象
Proxy的优势如下:
- 针对对象:针对整个对象,而不是对象的某个属性,所以也就不需要对
keys进行遍历 - 支持数组:
Proxy不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本,而且标准的就是最好的 Proxy的第二个参数可以有13种拦截方:不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利
proxy详细使用点击查看 (opens new window)
Object.defineProperty的优势如下:
兼容性好,支持
IE9,而Proxy的存在浏览器兼容性问题,而且无法用polyfill磨平
defineProperty的属性值有哪些
Object.defineProperty(obj, prop, descriptor) // obj 要定义属性的对象 // prop 要定义或修改的属性的名称 // descriptor 要定义或修改的属性描述符 Object.defineProperty(obj,"name",{ value:"poetry", // 初始值 writable:true, // 该属性是否可写入 enumerable:true, // 该属性是否可被遍历得到(for...in, Object.keys等) configurable:true, // 定该属性是否可被删除,且除writable外的其他描述符是否可被修改 get: function() {}, set: function(newVal) {} })@程序员poetry: 代码已经复制到剪贴板
相关代码如下
import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑 import { isObject } from "./util"; // 工具方法 export function reactive(target) { // 根据不同参数创建不同响应式对象 return createReactiveObject(target, mutableHandlers); } function createReactiveObject(target, baseHandler) { if (!isObject(target)) { return target; } const observed = new Proxy(target, baseHandler); return observed; } const get = createGetter(); const set = createSetter(); function createGetter() { return function get(target, key, receiver) { // 对获取的值进行放射 const res = Reflect.get(target, key, receiver); console.log("属性获取", key); if (isObject(res)) { // 如果获取的值是对象类型,则返回当前对象的代理对象 return reactive(res); } return res; }; } function createSetter() { return function set(target, key, value, receiver) { const oldValue = target[key]; const hadKey = hasOwn(target, key); const result = Reflect.set(target, key, value, receiver); if (!hadKey) { console.log("属性新增", key, value); } else if (hasChanged(value, oldValue)) { console.log("属性值被修改", key, value); } return result; }; } export const mutableHandlers = { get, // 当获取属性时调用此方法 set, // 当修改属性时调用此方法 };@程序员poetry: 代码已经复制到剪贴板
Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?
判断当前
Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。
监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?
我们可以判断
key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger
# 5 Vue中如何检测数组变化
前言
Vue 不能检测到以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue - 当你修改数组的长度时,例如:
vm.items.length = newLength
Vue 提供了以下操作方法
// Vue.set Vue.set(vm.items, indexOfItem, newValue) // vm.$set,Vue.set的一个别名 vm.$set(vm.items, indexOfItem, newValue) // Array.prototype.splice vm.items.splice(indexOfItem, 1, newValue)@程序员poetry: 代码已经复制到剪贴板
分析
数组考虑性能原因没有用
defineProperty对数组的每一项进行拦截,而是选择对7种数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写(AOP切片思想)
所以在 Vue 中修改数组的索引和长度是无法监控到的。需要通过以上 7 种变异方法修改数组才会触发数组对应的 watcher 进行更新
- 用函数劫持的方式,重写了数组方法,具体呢就是更改了数组的原型,更改成自己的,用户调数组的一些方法的时候,走的就是自己的方法,然后通知视图去更新
- 数组里每一项可能是对象,那么我就是会对数组的每一项进行观测,(且只有数组里的对象才能进行观测,观测过的也不会进行观测)
原理
Vue将data中的数组,进行了原型链重写。指向了自己定义的数组原型方法,这样当调用数组api时,可以通知依赖更新,如果数组中包含着引用类型。会对数组中的引用类型再次进行监控。

手写简版分析
let oldArray = Object.create(Array.prototype); ['shift', 'unshift', 'push', 'pop', 'reverse','sort'].forEach(method => { oldArray[method] = function() { // 这里可以触发页面更新逻辑 console.log('method', method) Array.prototype[method].call(this,...arguments); } }); let arr = [1,2,3]; arr.__proto__ = oldArray; arr.unshift(4);@程序员poetry: 代码已经复制到剪贴板
源码分析
// 拿到数组原型拷贝一份 const arrayProto = Array.prototype // 然后将arrayMethods继承自数组原型 // 这里是面向切片编程思想(AOP)--不破坏封装的前提下,动态的扩展功能 export const arrayMethods = Object.create(arrayProto) const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { // 重写原型方法 const original = arrayProto[method] // 调用原数组的方法 def(arrayMethods, method, function mutator (...args) { // 这里保留原型方法的执行结果 const result = original.apply(this, args) // 这句话是关键 // this代表的就是数据本身 比如数据是{a:[1,2,3]} 那么我们使用a.push(4) this就是a ob就是a.__ob__ 这个属性就是上段代码增加的 代表的是该数据已经被响应式观察过了指向Observer实例 const ob = this.__ob__ // 这里的标志就是代表数组有新增操作 let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } // 如果有新增的元素 inserted是一个数组 调用Observer实例的observeArray对数组每一项进行观测 if (inserted) ob.observeArray(inserted) ob.dep.notify() // 当调用数组方法后,手动通知视图更新 return result }) }) this.observeArray(value) // 进行深度监控@程序员poetry: 代码已经复制到剪贴板
vue3:改用proxy,可直接监听对象数组的变化
# 6 Vue中如何进行依赖收集?
- 每个属性都有自己的
dep属性,存放他所依赖的watcher,当属性变化之后会通知自己对应的watcher去更新 - 默认会在初始化时调用
render函数,此时会触发属性依赖收集dep.depend - 当属性发生修改时会触发
watcher更新dep.notify()

依赖收集简版
let obj = { name: 'poetry', age: 20 }; class Dep { constructor() { this.subs = [] // subs [watcher] } depend() { this.subs.push(Dep.target) } notify() { this.subs.forEach(watcher => watcher.update()) } } Dep.target = null; observer(obj); // 响应式属性劫持 // 依赖收集 所有属性都会增加一个dep属性, // 当渲染的时候取值了 ,这个dep属性 就会将渲染的watcher收集起来 // 数据更新 会让watcher重新执行 // 观察者模式 // 渲染组件时 会创建watcher class Watcher { constructor(render) { this.get(); } get() { Dep.target = this; render(); // 执行render Dep.target = null; } update() { this.get(); } } const render = () => { console.log(obj.name); // obj.name => get方法 } // 组件是watcher、计算属性是watcher new Watcher(render); function observer(value) { // proxy reflect if (typeof value === 'object' && typeof value !== null) for (let key in value) { defineReactive(value, key, value[key]); } } function defineReactive(obj, key, value) { // 创建一个dep let dep = new Dep(); // 递归观察子属性 observer(value); Object.defineProperty(obj, key, { get() { // 收集对应的key 在哪个方法(组件)中被使用 if (Dep.target) { // watcher dep.depend(); // 这里会建立 dep 和watcher的关系 } return value; }, set(newValue) { if (newValue !== value) { observer(newValue); value = newValue; // 让key对应的方法(组件重新渲染)重新执行 dep.notify() } } }) } // 模拟数据获取,触发getter obj.name = 'poetries' // 一个属性一个dep,一个属性可以对应多个watcher(一个属性可以在任何组件中使用、在多个组件中使用) // 一个dep 对应多个watcher // 一个watcher 对应多个dep (一个视图对应多个属性) // dep 和 watcher是多对多的关系@程序员poetry: 代码已经复制到剪贴板
# 7 Vue实例挂载的过程中发生了什么
TIP
分析
挂载过程完成了最重要的两件事:
- 初始化
- 建立更新机制
把这两件事说清楚即可!
回答范例
- 挂载过程指的是
app.mount()过程,这个过程中整体上做了两件事:初始化和建立更新机制 - 初始化会创建组件实例、初始化组件状态,创建各种响应式数据
- 建立更新机制这一步会立即执行一次组件更新函数,这会首次执行组件渲染函数并执行
patch将前面获得vnode转换为dom;同时首次执行渲染函数会创建它内部响应式数据之间和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数
来看一下源码,在src/core/instance/index.js 中
function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) }@程序员poetry: 代码已经复制到剪贴板
可以看到 Vue 只能通过 new 关键字初始化,然后会调用 this._init 方法, 该方法在 src/core/instance/init.js 中定义
Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to avoid this being observed vm._isVue = true // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } if (vm.$options.el) { vm.$mount(vm.$options.el) } }@程序员poetry: 代码已经复制到剪贴板
Vue初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher等
# 8 理解Vue运行机制全局概览
# 全局概览
首先我们来看一下笔者画的内部流程图。

大家第一次看到这个图一定是一头雾水的,没有关系,我们来逐个讲一下这些模块的作用以及调用关系。相信讲完之后大家对Vue.js内部运行机制会有一个大概的认识。
# 初始化及挂载

在
new Vue()之后。 Vue 会调用_init函数进行初始化,也就是这里的init过程,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。其中最重要的是通过Object.defineProperty设置setter与getter函数,用来实现「响应式」以及「依赖收集」,后面会详细讲到,这里只要有一个印象即可。
初始化之后调用
$mount会挂载组件,如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要进行「编译」步骤。
# 编译
compile编译可以分成 parse、optimize 与 generate 三个阶段,最终需要得到 render function。

1. parse
parse 会用正则等方式解析 template 模板中的指令、class、style等数据,形成AST。
2. optimize
optimize的主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当update更新界面时,会有一个patch的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了patch的性能。
3. generate
generate是将 AST 转化成render function字符串的过程,得到结果是render的字符串以及 staticRenderFns 字符串。
- 在经历过
parse、optimize与generate这三个阶段以后,组件中就会存在渲染VNode所需的render function了。
# 响应式
接下来也就是 Vue.js 响应式核心部分。

这里的
getter跟setter已经在之前介绍过了,在init的时候通过Object.defineProperty进行了绑定,它使得当被设置的对象被读取的时候会执行getter函数,而在当被赋值的时候会执行setter函数。
- 当
render function被渲染的时候,因为会读取所需对象的值,所以会触发getter函数进行「依赖收集」,「依赖收集」的目的是将观察者Watcher对象存放到当前闭包中的订阅者Dep的subs中。形成如下所示的这样一个关系。

在修改对象的值的时候,会触发对应的
setter,setter通知之前「依赖收集」得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用update来更新视图,当然这中间还有一个patch的过程以及使用队列来异步更新的策略,这个我们后面再讲。
# Virtual DOM
我们知道,
render function会被转化成VNode节点。Virtual DOM其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
比如说下面这样一个例子:
{ tag: 'div', /*说明这是一个div标签*/ children: [ /*存放该标签的子节点*/ { tag: 'a', /*说明这是一个a标签*/ text: 'click me' /*标签的内容*/ } ] }@程序员poetry: 代码已经复制到剪贴板
渲染后可以得到
<div> <a>click me</a> </div>@程序员poetry: 代码已经复制到剪贴板
这只是一个简单的例子,实际上的节点有更多的属性来标志节点,比如 isStatic (代表是否为静态节点)、 isComment (代表是否为注释节点)等。
# 更新视图

- 前面我们说到,在修改一个对象值的时候,会通过
setter -> Watcher -> update的流程来修改对应的视图,那么最终是如何更新视图的呢? - 当数据变化后,执行 render function 就可以得到一个新的 VNode 节点,我们如果想要得到新的视图,最简单粗暴的方法就是直接解析这个新的
VNode节点,然后用innerHTML直接全部渲染到真实DOM中。但是其实我们只对其中的一小块内容进行了修改,这样做似乎有些「浪费」。 - 那么我们为什么不能只修改那些「改变了的地方」呢?这个时候就要介绍我们的「
patch」了。我们会将新的VNode与旧的VNode一起传入patch进行比较,经过 diff 算法得出它们的「差异」。最后我们只需要将这些「差异」的对应 DOM 进行修改即可。
# 再看全局

回过头再来看看这张图,是不是大脑中已经有一个大概的脉络了呢?
# 9 如何理解Vue中模板编译原理
Vue的编译过程就是将template转化为render函数的过程
- 解析生成AST树 将
template模板转化成AST语法树,使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理 - 标记优化 对静态语法做静态标记
markup(静态节点如div下有p标签内容不会变化)diff来做优化 静态节点跳过diff操作Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用- 等待后续节点更新,如果是静态的,不会在比较
children了
- 代码生成 编译的最后一步是将优化后的
AST树转换为可执行的代码
回答范例
思路
- 引入
vue编译器概念 - 说明编译器的必要性
- 阐述编译器工作流程
回答范例
Vue中有个独特的编译器模块,称为compiler,它的主要作用是将用户编写的template编译为js中可执行的render函数。- 之所以需要这个编译过程是为了便于前端能高效的编写视图模板。相比而言,我们还是更愿意用
HTML来编写视图,直观且高效。手写render函数不仅效率底下,而且失去了编译期的优化能力。 - 在
Vue中编译器会先对template进行解析,这一步称为parse,结束之后会得到一个JS对象,我们称为抽象语法树AST,然后是对AST进行深加工的转换过程,这一步成为transform,最后将前面得到的AST生成为JS代码,也就是render函数
可能的追问
Vue中编译器何时执行?

在
new Vue()之后。Vue会调用_init函数进行初始化,也就是这里的 init过程,它会初始化生命周期、事件、props、methods、data、computed与watch等。其中最重要的是通过Object.defineProperty设置setter与getter函数,用来实现「响应式」以及「依赖收集」
- 初始化之后调用
$mount会挂载组件,如果是运行时编译,即不存在render function但是存在template的情况,需要进行「编译」步骤 compile编译可以分成parse、optimize与generate三个阶段,最终需要得到render function
React有没有编译器?
react 使用babel将JSX语法解析
<div id="app"></div> <script> let vm = new Vue({ el: '#app', template: `<div> // <span>hello world</span> 是静态节点 <span>hello world</span> // <p>{{name}}</p> 是动态节点 <p>{{name}}</p> </div>`, data() { return { name: 'test' } } }); </script>@程序员poetry: 代码已经复制到剪贴板
源码分析
export function compileToFunctions(template) { // 我们需要把html字符串变成render函数 // 1.把html代码转成ast语法树 ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法 // 很多库都运用到了ast 比如 webpack babel eslint等等 let ast = parse(template); // 2.优化静态节点:对ast树进行标记,标记静态节点 if (options.optimize !== false) { optimize(ast, options); } // 3.通过ast 重新生成代码 // 我们最后生成的代码需要和render函数一样 // 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world")))) // _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本 let code = generate(ast); // 使用with语法改变作用域为this 之后调用render函数可以使用call改变this 方便code里面的变量取值 let renderFn = new Function(`with(this){return ${code}}`); return renderFn; }@程序员poetry: 代码已经复制到剪贴板
# Vue complier 实现
- 模板解析这种事,本质是将数据转化为一段
html,最开始出现在后端,经过各种处理吐给前端。随着各种mv*的兴起,模板解析交由前端处理。 - 总的来说,
Vue complier是将template转化成一个render字符串。
可以简单理解成以下步骤:
parse过程,将template利用正则转化成AST抽象语法树。optimize过程,标记静态节点,后diff过程跳过静态节点,提升性能。generate过程,生成render字符串
# 10 Vue生命周期相关
# Vue的生命周期方法有哪些
Vue实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是Vue的生命周期Vue生命周期总共分为8个阶段创建前/后,载入前/后,更新前/后,销毁前/后
beforeCreate=>created=>beforeMount=>Mounted=>beforeUpdate=>updated=>beforeDestroy=>destroyed。keep-alive下:activateddeactivated
| 生命周期vue2 | 生命周期vue3 | 描述 |
|---|---|---|
beforeCreate | beforeCreate | 在实例初始化之后,数据观测(data observer) 之前被调用。 |
created | created | 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el |
beforeMount | beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用 |
mounted | mounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子 |
beforeUpdate | beforeUpdate | 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前 |
updated | updated | 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子 |
beforeDestroy | beforeUnmount | 实例销毁之前调用。在这一步,实例仍然完全可用 |
destroyed | unmounted | 实例销毁后调用。调用后, Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。 |
其他几个生命周期
| 生命周期vue2 | 生命周期vue3 | 描述 |
|---|---|---|
activated | activated | keep-alive专属,组件被激活时调用 |
deactivated | deactivated | keep-alive专属,组件被销毁时调用 |
errorCaptured | errorCaptured | 捕获一个来自子孙组件的错误时被调用 |
| - | renderTracked | 调试钩子,响应式依赖被收集时调用 |
| - | renderTriggered | 调试钩子,响应式依赖被触发时调用 |
| - | serverPrefetch | ssr only,组件实例在服务器上被渲染前调用 |
- 要掌握每个生命周期内部可以做什么事
beforeCreate通常用于插件开发中执行一些初始化任务created组件初始化完毕,可以访问各种数据,获取接口数据等mounted实例已经挂载完成,可以进行一些DOM操作beforeUpdate此时view层还未更新,可用于获取更新前各种状态。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。updated完成view层的更新,更新后,所有状态已是最新。可以执行依赖于DOM的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。destroyed可以执行一些优化操作,清空定时器,解除绑定事件- vue3
beforeunmount:实例被销毁前调用,可用于一些定时器或订阅的取消 - vue3
unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器

<div id="app">{{name}}</div> <script> const vm = new Vue({ data(){ return {name:'poetries'} }, el: '#app', beforeCreate(){ // 数据观测(data observer) 和 event/watcher 事件配置之前被调用。 console.log('beforeCreate'); }, created(){ // 属性和方法的运算, watch/event 事件回调。这里没有$el console.log('created') }, beforeMount(){ // 相关的 render 函数首次被调用。 console.log('beforeMount') }, mounted(){ // 被新创建的 vm.$el 替换 console.log('mounted') }, beforeUpdate(){ // 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。 console.log('beforeUpdate') }, updated(){ // 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。 console.log('updated') }, beforeDestroy(){ // 实例销毁之前调用 实例仍然完全可用 console.log('beforeDestroy') }, destroyed(){ // 所有东西都会解绑定,所有的事件监听器会被移除 console.log('destroyed') } }); setTimeout(() => { vm.name = 'poetry'; setTimeout(() => { vm.$destroy() }, 1000); }, 1000); </script>@程序员poetry: 代码已经复制到剪贴板
- 组合式API生命周期钩子
你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。
下表包含如何在 setup() 内部调用生命周期钩子:
| 选项式 API | Hook inside setup |
|---|---|
beforeCreate | 不需要* |
created | 不需要* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
因为
setup是围绕beforeCreate和created生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在setup函数中编写
export default { setup() { // mounted onMounted(() => { console.log('Component is mounted!') }) } }@程序员poetry: 代码已经复制到剪贴板
setup和created谁先执行?
beforeCreate:组件被创建出来,组件的methods和data还没初始化好setup:在beforeCreate和created之间执行created:组件被创建出来,组件的methods和data已经初始化好了
由于在执行
setup的时候,created还没有创建好,所以在setup函数内我们是无法使用data和methods的。所以vue为了让我们避免错误的使用,直接将setup函数内的this执行指向undefined
import { ref } from "vue" export default { // setup函数是组合api的入口函数,注意在组合api中定义的变量或者方法,要在template响应式需要return{}出去 setup(){ let count = ref(1) function myFn(){ count.value +=1 } return {count,myFn} }, }@程序员poetry: 代码已经复制到剪贴板
- 其他问题
- 什么是vue生命周期? Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为
Vue的生命周期。 - vue生命周期的作用是什么? 它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。
- vue生命周期总共有几个阶段? 它可以总共分为
8个阶段:创建前/后、载入前/后、更新前/后、销毁前/销毁后。 - 第一次页面加载会触发哪几个钩子? 会触发下面这几个
beforeCreate、created、beforeMount、mounted。 - 你的接口请求一般放在哪个生命周期中? 接口请求一般放在
mounted中,但需要注意的是服务端渲染时不支持mounted,需要放到created中 - DOM 渲染在哪个周期中就已经完成? 在
mounted中,- 注意
mounted不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用vm.$nextTick替换掉mounted
mounted: function () { this.$nextTick(function () { // Code that will run only after the // entire view has been rendered }) }@程序员poetry: 代码已经复制到剪贴板 - 注意
# 父组件可以监听到子组件的生命周期吗
比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:
// Parent.vue <Child @mounted="doSomething"/> // Child.vue mounted() { this.$emit("mounted"); }@程序员poetry: 代码已经复制到剪贴板
以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:
// Parent.vue <Child @hook:mounted="doSomething" ></Child> doSomething() { console.log('父组件监听到 mounted 钩子函数 ...'); }, // Child.vue mounted(){ console.log('子组件触发 mounted 钩子函数 ...'); }, // 以上输出顺序为: // 子组件触发 mounted 钩子函数 ... // 父组件监听到 mounted 钩子函数 ...@程序员poetry: 代码已经复制到剪贴板
当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听
# Vue生命周期钩子是如何实现的
vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法- 内部会对钩子函数进行处理,将钩子函数维护成数组的形式
Vue的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)
<script> // Vue.options 中会存放所有全局属性 // 会用自身的 + Vue.options 中的属性进行合并 // Vue.mixin({ // beforeCreate() { // console.log('before 0') // }, // }) debugger; const vm = new Vue({ el: '#app', beforeCreate: [ function() { console.log('before 1') }, function() { console.log('before 2') } ] }); console.log(vm); </script>@程序员poetry: 代码已经复制到剪贴板
相关代码如下
export function callHook(vm, hook) { // 依次执行生命周期对应的方法 const handlers = vm.$options[hook]; if (handlers) { for (let i = 0; i < handlers.length; i++) { handlers[i].call(vm); //生命周期里面的this指向当前实例 } } } // 调用的时候 Vue.prototype._init = function (options) { const vm = this; vm.$options = mergeOptions(vm.constructor.options, options); callHook(vm, "beforeCreate"); //初始化数据之前 // 初始化状态 initState(vm); callHook(vm, "created"); //初始化数据之后 if (vm.$options.el) { vm.$mount(vm.$options.el); } }; // 销毁实例实现 Vue.prototype.$destory = function() { // 触发钩子 callHook(vm, 'beforeDestory') // 自身及子节点 remove() // 删除依赖 watcher.teardown() // 删除监听 vm.$off() // 触发钩子 callHook(vm, 'destoryed') }@程序员poetry: 代码已经复制到剪贴板
原理流程图

# Vue 的父子组件生命周期钩子函数执行顺序
- 渲染顺序:先父后子,完成顺序:先子后父
- 更新顺序:父更新导致子更新,子更新完成后父
- 销毁顺序:先父后子,完成顺序:先子后父
加载渲染过程
父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted。子组件先挂载,然后到父组件
子组件更新过程
父 beforeUpdate->子 beforeUpdate->子 updated->父 updated
父组件更新过程
父 beforeUpdate->父 updated
销毁过程
父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed
之所以会这样是因为
Vue创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加mounted钩子到队列,等到patch结束再执行它们,可见子组件的mounted钩子是先进入到队列中的,因此等到patch结束执行这些钩子时也先执行。

function patch (oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] // 定义收集所有组件的insert hook方法的数组 // somthing ... createElm( vnode, insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) )// somthing... // 最终会依次调用收集的insert hook invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); return vnode.elm } function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { // createChildren 会递归创建儿子组件 createChildren(vnode, children, insertedVnodeQueue) // something... } // 将组件的vnode插入到数组中 function invokeCreateHooks (vnode, insertedVnodeQueue) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode) } i = vnode.data.hook // Reuse variable if (isDef(i)) { if (isDef(i.create)) i.create(emptyNode, vnode) if (isDef(i.insert)) insertedVnodeQueue.push(vnode) } } // insert方法中会依次调用mounted方法 insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance._isMounted) { componentInstance._isMounted = true callHook(componentInstance, 'mounted') } } function invokeInsertHook (vnode, queue, initial) { // delay insert hooks for component root nodes, invoke them after the // element is really inserted if (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data.pendingInsert = queue } else { for (let i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]); // 调用insert方法 } } } Vue.prototype.$destroy = function () { callHook(vm, 'beforeDestroy') // invoke destroy hooks on current rendered tree vm.__patch__(vm._vnode, null) // 先销毁儿子 // fire destroyed hook callHook(vm, 'destroyed') }@程序员poetry: 代码已经复制到剪贴板
# 11 Vue.mixin的使用场景和原理
- 在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立,可以通过
Vue的mixin功能抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”;如果混入的数据和本身组件的数据冲突,会以组件的数据为准 mixin有很多缺陷如:命名冲突、依赖问题、数据来源问题
基本使用
<script> // Vue.options Vue.mixin({ // 如果他是对象 每个组件都用mixin里的对象进行合并 data(){ return {a: 1,b: 2} } }); // Vue.extend Vue.component('my',{ // 组件必须是函数 Vue.extend => render(xxx) data(){ return {x:1} } }) // 没有 new 没有实例 _init() // const vm = this new Vue({ el:'#app', data(){ // 根可以不是函数 return {c:3} } }) </script>@程序员poetry: 代码已经复制到剪贴板
相关源码
export default function initMixin(Vue){ Vue.mixin = function (mixin) { // 合并对象 this.options=mergeOptions(this.options,mixin) }; } }; // src/util/index.js // 定义生命周期 export const LIFECYCLE_HOOKS = [ "beforeCreate", "created", "beforeMount", "mounted", "beforeUpdate", "updated", "beforeDestroy", "destroyed", ]; // 合并策略 const strats = {}; // mixin核心方法 export function mergeOptions(parent, child) { const options = {}; // 遍历父亲 for (let k in parent) { mergeFiled(k); } // 父亲没有 儿子有 for (let k in child) { if (!parent.hasOwnProperty(k)) { mergeFiled(k); } } //真正合并字段方法 function mergeFiled(k) { // strats合并策略 if (strats[k]) { options[k] = strats[k](parent[k], child[k]); } else { // 默认策略 options[k] = child[k] ? child[k] : parent[k]; } } return options; }@程序员poetry: 代码已经复制到剪贴板
# 12 Vue组件data为什么必须是个函数?
一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果
data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数,
简版理解
// 1.组件的渲染流程 调用Vue.component -> Vue.extend -> 子类 -> new 子类 // Vue.extend 根据用户定义产生一个新的类 function Vue() {} function Sub() { // 会将data存起来 this.data = this.constructor.options.data(); } Vue.extend = function(options) { Sub.options = options; // 静态属性 return Sub; } let Child = Vue.extend({ data:()=>( { name: 'zf' }) }); // 两个组件就是两个实例, 希望数据互不感染 let child1 = new Child(); let child2 = new Child(); console.log(child1.data.name); child1.data.name = 'poetry'; console.log(child2.data.name); // 根不需要 任何的合并操作 根才有vm属性 所以他可以是函数和对象 但是组件mixin他们都没有vm 所以我就可以判断 当前data是不是个函数@程序员poetry: 代码已经复制到剪贴板
相关源码
// 源码位置 src/core/global-api/extend.js export function initExtend (Vue: GlobalAPI) { Vue.extend = function (extendOptions: Object): Function { extendOptions = extendOptions || {} const Super = this const SuperId = Super.cid const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) if (cachedCtors[SuperId]) { return cachedCtors[SuperId] } const name = extendOptions.name || Super.options.name if (process.env.NODE_ENV !== 'production' && name) { validateComponentName(name) } const Sub = function VueComponent (options) { this._init(options) } // 子类继承大Vue父类的原型 Sub.prototype = Object.create(Super.prototype) Sub.prototype.constructor = Sub Sub.cid = cid++ Sub.options = mergeOptions( Super.options, extendOptions ) Sub['super'] = Super // For props and computed properties, we define the proxy getters on // the Vue instances at extension time, on the extended prototype. This // avoids Object.defineProperty calls for each instance created. if (Sub.options.props) { initProps(Sub) } if (Sub.options.computed) { initComputed(Sub) } // allow further extension/mixin/plugin usage Sub.extend = Super.extend Sub.mixin = Super.mixin Sub.use = Super.use // create asset registers, so extended classes // can have their private assets too. ASSET_TYPES.forEach(function (type) { Sub[type] = Super[type] }) // enable recursive self-lookup if (name) { Sub.options.components[name] = Sub // 记录自己 在组件中递归自己 -> jsx } // keep a reference to the super options at extension time. // later at instantiation we can check if Super's options have // been updated. Sub.superOptions = Super.options Sub.extendOptions = extendOptions Sub.sealedOptions = extend({}, Sub.options) // cache constructor cachedCtors[SuperId] = Sub return Sub } }@程序员poetry: 代码已经复制到剪贴板
# 13 nextTick在哪里使用?原理是?
nextTick中的回调是在下次DOM更新循环结束之后执行延迟回调,用于获得更新后的DOM- 在修改数据之后立即使用这个方法,获取更新后的
DOM - 主要思路就是采用
微任务优先的方式调用异步方法去执行nextTick包装的方法
nextTick方法主要是使用了宏任务和微任务,定义了一个异步方法.多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。所以这个nextTick方法就是异步方法
根据执行环境分别尝试采用
- 先采用
Promise Promise不支持,再采用MutationObserverMutationObserver不支持,再采用setImmediate- 如果以上都不行则采用
setTimeout - 最后执行
flushCallbacks,把callbacks里面的数据依次执行

回答范例
nextTick中的回调是在下次DOM更新循环结束之后执行延迟回调,用于获得更新后的DOMVue有个异步更新策略,意思是如果数据变化,Vue不会立刻更新DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick- 开发时,有两个场景我们会用到
nextTick
created中想要获取DOM时- 响应式数据变化后获取
DOM更新后的状态,比如希望获取列表更新后的高度
nextTick签名如下:function nextTick(callback?: () => void): Promise<void>
所以我们只需要在传入的回调函数中访问最新DOM状态即可,或者我们可以await nextTick()方法返回的Promise之后做这件事
- 在
Vue内部,nextTick之所以能够让我们看到DOM更新后的结果,是因为我们传入的callback会被添加到队列刷新函数(flushSchedulerQueue)的后面,这样等队列内部的更新函数都执行完毕,所有DOM操作也就结束了,callback自然能够获取到最新的DOM值
基本使用
const vm = new Vue({ el: '#app', data() { return { a: 1 } } }); // vm.$nextTick(() => {// [nextTick回调函数fn,内部更新flushSchedulerQueue] // console.log(vm.$el.innerHTML) // }) // 是将内容维护到一个数组里,最终按照顺序顺序。 第一次会开启一个异步任务 vm.a = 'test'; // 修改了数据后并不会马上更新视图 vm.$nextTick(() => {// [nextTick回调函数fn,内部更新flushSchedulerQueue] console.log(vm.$el.innerHTML) }) // nextTick中的方法会被放到 更新页面watcher的后面去@程序员poetry: 代码已经复制到剪贴板
相关代码如下

// src/core/utils/nextTick let callbacks = []; let pending = false; function flushCallbacks() { pending = false; //把标志还原为false // 依次执行回调 for (let i = 0; i < callbacks.length; i++) { callbacks[i](); } } let timerFunc; //定义异步方法 采用优雅降级 if (typeof Promise !== "undefined") { // 如果支持promise const p = Promise.resolve(); timerFunc = () => { p.then(flushCallbacks); }; } else if (typeof MutationObserver !== "undefined") { // MutationObserver 主要是监听dom变化 也是一个异步方法 let counter = 1; const observer = new MutationObserver(flushCallbacks); const textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true, }); timerFunc = () => { counter = (counter + 1) % 2; textNode.data = String(counter); }; } else if (typeof setImmediate !== "undefined") { // 如果前面都不支持 判断setImmediate timerFunc = () => { setImmediate(flushCallbacks); }; } else { // 最后降级采用setTimeout timerFunc = () => { setTimeout(flushCallbacks, 0); }; } export function nextTick(cb) { // 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组 callbacks.push(cb); if (!pending) { // 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false pending = true; timerFunc(); } }@程序员poetry: 代码已经复制到剪贴板
数据更新的时候内部会调用nextTick
// src/core/observer/scheduler.js export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } // 把更新方法放到数组中维护[nextTick回调函数,更新函数flushSchedulerQueue] /** * vm.a = 'test'; // 修改了数据后并不会马上更新视图 vm.$nextTick(() => {// [fn,更新] console.log(vm.$el.innerHTML) }) */ nextTick(flushSchedulerQueue) } } }@程序员poetry: 代码已经复制到剪贴板
# 14 computed和watch相关
# computed和watch区别
- 当页面中有某些数据依赖其他数据进行变动的时候,可以使用计算属性
computed
Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。 适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理

<template>{{fullName}}</template> export default { data(){ return { firstName: 'zhang', lastName: 'san', } }, computed:{ fullName: function(){ return this.firstName + ' ' + this.lastName } } }@程序员poetry: 代码已经复制到剪贴板
watch用于观察和监听页面上的vue实例,如果要在数据变化的同时进行异步操作或者是比较大的开销,那么watch为最佳选择
Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销

<template>{{fullName}}</template> export default { data(){ return { firstName: 'zhang', lastName: 'san', fullName: 'zhang san' } }, watch:{ firstName(val) { this.fullName = val + ' ' + this.lastName }, lastName(val) { this.fullName = this.firstName + ' ' + val } } }@程序员poetry: 代码已经复制到剪贴板
computed:
computed是计算属性,也就是计算值,它更多用于计算值的场景computed具有缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算computed适用于计算比较消耗性能的计算场景
watch:
- 更多的是「观察」的作用,类似于某些数据的监听回调,用于观察
props$emit或者本组件的值,当数据变化时来执行回调进行后续操作 - 无缓存性,页面重新渲染时值不变化也会执行
小结:
computed和watch都是基于watcher来实现的computed属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行watch是监控值的变化,当值发生变化时调用其对应的回调函数- 当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为
computed - 如果你需要在某个数据变化时做一些事情,使用
watch来观察这个数据变化
回答范例
思路分析
- 先看
computed,watch两者定义,列举使用上的差异 - 列举使用场景上的差异,如何选择
- 使用细节、注意事项
vue3变化
computed特点:具有响应式的返回值
const count = ref(1) const plusOne = computed(() => count.value + 1)@程序员poetry: 代码已经复制到剪贴板
watch特点:侦测变化,执行回调
const state = reactive({ count: 0 }) watch( () => state.count, (count, prevCount) => { /* ... */ } )@程序员poetry: 代码已经复制到剪贴板
回答范例
- 计算属性可以从组件数据派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,
computed和methods的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch没有返回值,但可以执行异步操作等复杂逻辑 - 计算属性常用场景是简化行内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。侦听器常用场景是状态变化之后做一些额外的DOM操作或者异步操作。选择采用何用方案时首先看是否需要派生出新值,基本能用计算属性实现的方式首选计算属性.
- 使用过程中有一些细节,比如计算属性也是可以传递对象,成为既可读又可写的计算属性。
watch可以传递对象,设置deep、immediate等选项 vue3中watch选项发生了一些变化,例如不再能侦测一个点操作符之外的字符串形式的表达式;reactivity API中新出现了watch、watchEffect可以完全替代目前的watch选项,且功能更加强大
基本使用
// src/core/observer:45; // 渲染watcher / computed watcher / watch const vm = new Vue({ el: '#app', data: { firstname:'张', lastname:'三' }, computed:{ // watcher => firstname lastname // computed 只有取值时才执行 // Object.defineProperty .get fullName(){ // firstName lastName 会收集fullName计算属性 return this.firstname + this.lastname } }, watch:{ firstname(newVal,oldVal){ console.log(newVal) } } }); setTimeout(() => { debugger; vm.firstname = '赵' }, 1000);@程序员poetry: 代码已经复制到剪贴板
相关源码
// 初始化state function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } // 初始化计算属性 if (opts.computed) initComputed(vm, opts.computed) // 初始化watch if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } } // 计算属性取值函数 function createComputedGetter (key) { return function computedGetter () { const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { if (watcher.dirty) { // 如果值依赖的值发生变化,就会进行重新求值 watcher.evaluate(); // this.firstname lastname } if (Dep.target) { // 让计算属性所依赖的属性 收集渲染watcher watcher.depend() } return watcher.value } } } // watch的实现 Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { const vm: Component = this debugger; if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true const watcher = new Watcher(vm, expOrFn, cb, options) // 创建watcher,数据更新调用cb if (options.immediate) { try { cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } return function unwatchFn () { watcher.teardown() } }@程序员poetry: 代码已经复制到剪贴板

# vue3中 watch、watchEffect区别
watch是惰性执行,也就是只有监听的值发生变化的时候才会执行,但是watchEffect不同,每次代码加载watchEffect都会执行(忽略watch第三个参数的配置,如果修改配置项也可以实现立即执行)watch需要传递监听的对象,watchEffect不需要watch只能监听响应式数据:ref定义的属性和reactive定义的对象,如果直接监听reactive定义对象中的属性是不允许的(会报警告),除非使用函数转换一下。其实就是官网上说的监听一个getterwatchEffect如果监听reactive定义的对象是不起作用的,只能监听对象中的属性
看一下watchEffect的代码
<template> <div> 请输入firstName: <input type="text" v-model="firstName"> </div> <div> 请输入lastName: <input type="text" v-model="lastName"> </div> <div> 请输入obj.text: <input type="text" v-model="obj.text"> </div> <div> 【obj.text】 {{obj.text}} </div> </template> <script> import {ref, reactive, watch, watchEffect} from 'vue' export default { name: "HelloWorld", props: { msg: String, }, setup(props,content){ let firstName = ref('') let lastName = ref('') let obj= reactive({ text:'hello' }) watchEffect(()=>{ console.log('触发了watchEffect'); console.log(`组合后的名称为:${firstName.value} ${lastName.value}`) }) return{ obj, firstName, lastName } } }; </script>@程序员poetry: 代码已经复制到剪贴板

改造一下代码
watchEffect(()=>{ console.log('触发了watchEffect'); // 这里我们不使用firstName.value/lastName.value ,相当于是监控整个ref,对应第四点上面的结论 console.log(`组合后的名称为:${firstName} ${lastName}`) })@程序员poetry: 代码已经复制到剪贴板

watchEffect(()=>{ console.log('触发了watchEffect'); console.log(obj); })@程序员poetry: 代码已经复制到剪贴板

稍微改造一下
let obj = reactive({ text:'hello' }) watchEffect(()=>{ console.log('触发了watchEffect'); console.log(obj.text); })@程序员poetry: 代码已经复制到剪贴板

再看一下watch的代码,验证一下
let obj= reactive({ text:'hello' }) // watch是惰性执行, 默认初始化之后不会执行,只有值有变化才会触发,可通过配置参数实现默认执行 watch(obj, (newValue, oldValue) => { // 回调函数 console.log('触发监控更新了new', newValue); console.log('触发监控更新了old', oldValue); },{ // 配置immediate参数,立即执行,以及深层次监听 immediate: true, deep: true })@程序员poetry: 代码已经复制到剪贴板

- 监控整个
reactive对象,从上面的图可以看到deep实际默认是开启的,就算我们设置为false也还是无效。而且旧值获取不到。 - 要获取旧值则需要监控对象的属性,也就是监听一个
getter,看下图

总结
- 如果定义了
reactive的数据,想去使用watch监听数据改变,则无法正确获取旧值,并且deep属性配置无效,自动强制开启了深层次监听。 - 如果使用
ref初始化一个对象或者数组类型的数据,会被自动转成reactive的实现方式,生成proxy代理对象。也会变得无法正确取旧值。 - 用任何方式生成的数据,如果接收的变量是一个
proxy代理对象,就都会导致watch这个对象时,watch回调里无法正确获取旧值。 - 所以当大家使用
watch监听对象时,如果在不需要使用旧值的情况,可以正常监听对象没关系;但是如果当监听改变函数里面需要用到旧值时,只能监听 对象.xxx`属性 的方式才行
watch和watchEffect异同总结
体验
watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数
const count = ref(0) watchEffect(() => console.log(count.value)) // -> logs 0 count.value++ // -> logs 1@程序员poetry: 代码已经复制到剪贴板
watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数
const state = reactive({ count: 0 }) watch( () => state.count, (count, prevCount) => { /* ... */ } )@程序员poetry: 代码已经复制到剪贴板
回答范例
watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数watchEffect(effect)是一种特殊watch,传入的函数既是依赖收集的数据源,也是回调函数。如果我们不关心响应式数据变化前后的值,只是想拿这些数据做些事情,那么watchEffect就是我们需要的。watch更底层,可以接收多种数据源,包括用于依赖收集的getter函数,因此它完全可以实现watchEffect的功能,同时由于可以指定getter函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用watchwatchEffect在使用时,传入的函数会立刻执行一次。watch默认情况下并不会执行回调函数,除非我们手动设置immediate选项- 从实现上来说,
watchEffect(fn)相当于watch(fn,fn,{immediate:true})
watchEffect定义如下
export function watchEffect( effect: WatchEffect, options?: WatchOptionsBase ): WatchStopHandle { return doWatch(effect, null, options) }@程序员poetry: 代码已经复制到剪贴板
watch定义如下
export function watch<T = any, Immediate extends Readonly<boolean> = false>( source: T | WatchSource<T>, cb: any, options?: WatchOptions<Immediate> ): WatchStopHandle { return doWatch(source as any, cb, options) }@程序员poetry: 代码已经复制到剪贴板
很明显watchEffect就是一种特殊的watch实现。
# Watch中的deep:true是如何实现的
当用户指定了
watch中的deep属性为true时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新
源码相关
get () { pushTarget(this) // 先将当前依赖放到 Dep.target上 let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { if (this.user) { handleError(e, vm, `getter for watcher "${this.expression}"`) } else { throw e } } finally { if (this.deep) { // 如果需要深度监控 traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法 }popTarget() }@程序员poetry: 代码已经复制到剪贴板
# Vue computed 实现
- 建立与其他属性(如:
data、Store)的联系; - 属性改变后,通知计算属性重新计算
实现时,主要如下
- 初始化
data, 使用Object.defineProperty把这些属性全部转为getter/setter。 - 初始化
computed, 遍历computed里的每个属性,每个computed属性都是一个watch实例。每个属性提供的函数作为属性的getter,使用Object.defineProperty转化。 Object.defineProperty getter依赖收集。用于依赖发生变化时,触发属性重新计算。- 若出现当前
computed计算属性嵌套其他computed计算属性时,先进行其他的依赖收集
# 15 Vue.set的实现原理
- 给对应和数组本身都增加了
dep属性 - 当给对象新增不存在的属性则触发对象依赖的
watcher去更新 - 当修改数组索引时,我们调用数组本身的
splice去更新数组(数组的响应式原理就是重新了splice等方法,调用splice就会触发视图更新)
基本使用
以下方法调用会改变原始数组:
push(),pop(),shift(),unshift(),splice(),sort(),reverse(),Vue.set( target, key, value )
- 调用方法:
Vue.set(target, key, value )target:要更改的数据源(可以是对象或者数组)key:要更改的具体数据value:重新赋的值
<div id="app">{{user.name}} {{user.age}}</div> <div id="app"></div> <script> // 1. 依赖收集的特点:给每个属性都增加一个dep属性,dep属性会进行收集,收集的是watcher // 2. vue会给每个对象也增加一个dep属性 const vm = new Vue({ el: '#app', data: { // vm._data user: {name:'poetry'} } }); // 对象的话:调用defineReactive在user对象上定义一个age属性,增加到响应式数据中,触发对象本身的watcher,ob.dep.notify()更新 // 如果是数组 通过调用 splice方法,触发视图更新 vm.$set(vm.user, 'age', 20); // 不能给根属性添加,因为给根添加属性 性能消耗太大,需要做很多处理 // 修改肯定是同步的 -> 更新都是一步的 queuewatcher </script>@程序员poetry: 代码已经复制到剪贴板
相关源码
// src/core/observer/index.js 44 export class Observer { // new Observer(value) value: any; dep: Dep; vmCount: number; // number of vms that have this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() // 给所有对象类型增加dep属性 } }@程序员poetry: 代码已经复制到剪贴板
// src/core/observer/index.js 201 export function set (target: Array<any> | Object, key: any, val: any): any { // 1.是开发环境 target 没定义或者是基础类型则报错 if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) } // 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图) if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) // 利用数组的splice变异方法触发响应式 target.splice(key, 1, val) return val } // 3.如果是对象本身的属性,则直接添加即可 if (key in target && !(key in Object.prototype)) { target[key] = val // 直接修改属性值 return val } // 4.如果是Vue实例 或 根数据data时 报错,(更新_data 无意义) const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } // 5.如果不是响应式的也不需要将其定义成响应式属性 if (!ob) { target[key] = val return val } // 6.将属性定义成响应式的 defineReactive(ob.value, key, val) // 通知视图更新 ob.dep.notify() return val }@程序员poetry: 代码已经复制到剪贴板
我们阅读以上源码可知,vm.$set 的实现原理是:
- 如果目标是数组,直接使用数组的
splice方法触发相应式; - 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用
defineReactive方法进行响应式处理(defineReactive方法就是Vue在初始化对象时,给对象属性采用Object.defineProperty动态添加getter和setter的功能所调用的方法)
# 16 Vue diff算法相关问题
# Vue为什么需要虚拟DOM?优缺点有哪些
由于在浏览器中操作
DOM是很昂贵的。频繁的操作DOM,会产生一定的性能问题。这就是虚拟Dom的产生原因。Vue2的Virtual DOM借鉴了开源库snabbdom的实现。Virtual DOM本质就是用一个原生的JS对象去描述一个DOM节点,是对真实DOM的一层抽象
优点:
- 保证性能下限: 框架的虚拟
DOM需要适配任何上层API可能产生的操作,它的一些DOM操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的DOM操作性能要好很多,因此框架的虚拟DOM至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限; - 无需手动操作 DOM: 我们不再需要手动去操作
DOM,只需要写好View-Model的代码逻辑,框架会根据虚拟DOM和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率; - 跨平台: 虚拟
DOM本质上是JavaScript对象,而DOM与平台强相关,相比之下虚拟DOM可以进行更方便地跨平台操作,例如服务器渲染、weex开发等等。
缺点:
- 无法进行极致优化:虽然虚拟
DOM+ 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟DOM无法进行针对性的极致优化。 - 首次渲染大量
DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。
虚拟 DOM 实现原理?
虚拟 DOM 的实现原理主要包括以下 3 部分:
- 用
JavaScript对象模拟真实DOM树,对真实DOM进行抽象; diff算法 — 比较两棵虚拟DOM树的差异;pach算法 — 将两个虚拟DOM对象的差异应用到真正的DOM树。
说说你对虚拟 DOM 的理解?回答范例
思路
vdom是什么- 引入
vdom的好处 vdom如何生成,又如何成为dom- 在后续的
diff中的作用
回答范例
- 虚拟
dom顾名思义就是虚拟的dom对象,它本身就是一个JavaScript对象,只不过它是通过不同的属性去描述一个视图结构 - 通过引入
vdom我们可以获得如下好处:
- 将真实元素节点抽象成
VNode,有效减少直接操作dom次数,从而提高程序性能- 直接操作
dom是有限制的,比如:diff、clone等操作,一个真实元素上有许多的内容,如果直接对其进行diff操作,会去额外diff一些没有必要的内容;同样的,如果需要进行clone那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到JavaScript对象上,那么就会变得简单了 - 操作
dom是比较昂贵的操作,频繁的dom操作容易引起页面的重绘和回流,但是通过抽象VNode进行中间处理,可以有效减少直接操作dom的次数,从而减少页面重绘和回流
- 直接操作
- 方便实现跨平台
- 同一
VNode节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是dom元素节点,渲染在Native( iOS、Android)变为对应的控件、可以实现SSR、渲染到WebGL中等等 Vue3中允许开发者基于VNode实现自定义渲染器(renderer),以便于针对不同平台进行渲染
- 同一
vdom如何生成?在vue中我们常常会为组件编写模板 -template, 这个模板会被编译器 -compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用render函数,返回的对象就是虚拟dom。但它们还不是真正的dom,所以会在后续的patch过程中进一步转化为dom。

- 挂载过程结束后,
vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render,此时就会生成新的vdom,和上一次的渲染结果diff就能得到变化的地方,从而转换为最小量的dom操作,高效更新视图
为什么要用vdom?案例解析
现在有一个场景,实现以下需求:
[ { name: "张三", age: "20", address: "北京"}, { name: "李四", age: "21", address: "武汉"}, { name: "王五", age: "22", address: "杭州"}, ]@程序员poetry: 代码已经复制到剪贴板
将该数据展示成一个表格,并且随便修改一个信息,表格也跟着修改。 用jQuery实现如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="container"></div> <button id="btn-change">改变</button> <script src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script> <script> const data = [{ name: "张三", age: "20", address: "北京" }, { name: "李四", age: "21", address: "武汉" }, { name: "王五", age: "22", address: "杭州" }, ]; //渲染函数 function render(data) { const $container = $('#container'); $container.html(''); const $table = $('<table>'); // 重绘一次 $table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>')); data.forEach(item => { //每次进入都重绘 $table.append($(`<tr><td>${item.name}</td><td>${item.age}</td><td>${item.address}</td></tr>`)) }) $container.append($table); } $('#btn-change').click(function () { data[1].age = 30; data[2].address = '深圳'; render(data); }); </script> </body> </html>@程序员poetry: 代码已经复制到剪贴板
- 这样点击按钮,会有相应的视图变化,但是你审查以下元素,每次改动之后,
table标签都得重新创建,也就是说table下面的每一个栏目,不管是数据是否和原来一样,都得重新渲染,这并不是理想中的情况,当其中的一栏数据和原来一样,我们希望这一栏不要重新渲染,因为DOM重绘相当消耗浏览器性能。 - 因此我们采用JS对象模拟的方法,将
DOM的比对操作放在JS层,减少浏览器不必要的重绘,提高效率。 - 当然有人说虚拟DOM并不比真实的
DOM快,其实也是有道理的。当上述table中的每一条数据都改变时,显然真实的DOM操作更快,因为虚拟DOM还存在js中diff算法的比对过程。所以,上述性能优势仅仅适用于大量数据的渲染并且改变的数据只是一小部分的情况。
如下DOM结构:
<ul id="list"> <li class="item">Item1</li> <li class="item">Item2</li> </ul>@程序员poetry: 代码已经复制到剪贴板
映射成虚拟DOM就是这样:
{ tag: "ul", attrs: { id: "list" }, children: [ { tag: "li", attrs: { className: "item" }, children: ["Item1"] }, { tag: "li", attrs: { className: "item" }, children: ["Item2"] } ] }@程序员poetry: 代码已经复制到剪贴板
使用snabbdom实现vdom
这是一个简易的实现
vdom功能的库,相比vue、react,对于vdom这块更加简易,适合我们学习vdom。vdom里面有两个核心的api,一个是h函数,一个是patch函数,前者用来生成vdom对象,后者的功能在于做虚拟dom的比对和将vdom挂载到真实DOM上
简单介绍一下这两个函数的用法:
h('标签名', {属性}, [子元素]) h('标签名', {属性}, [文本]) patch(container, vnode) // container为容器DOM元素 patch(vnode, newVnode)@程序员poetry: 代码已经复制到剪贴板
现在我们就来用snabbdom重写一下刚才的例子:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="container"></div> <button id="btn-change">改变</button> <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.min.js"></script> <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script> <script> let snabbdom = window.snabbdom; // 定义patch let patch = snabbdom.init([ snabbdom_class, snabbdom_props, snabbdom_style, snabbdom_eventlisteners ]); //定义h let h = snabbdom.h; const data = [{ name: "张三", age: "20", address: "北京" }, { name: "李四", age: "21", address: "武汉" }, { name: "王五", age: "22", address: "杭州" }, ]; data.unshift({name: "姓名", age: "年龄", address: "地址"}); let container = document.getElementById('container'); let vnode; const render = (data) => { let newVnode = h('table', {}, data.map(item => { let tds = []; for(let i in item) { if(item.hasOwnProperty(i)) { tds.push(h('td', {}, item[i] + '')); } } return h('tr', {}, tds); })); if(vnode) { patch(vnode, newVnode); } else { patch(container, newVnode); } vnode = newVnode; } render(data); let btnChnage = document.getElementById('btn-change'); btnChnage.addEventListener('click', function() { data[1].age = 30; data[2].address = "深圳"; //re-render render(data); }) </script> </body> </html>@程序员poetry: 代码已经复制到剪贴板

你会发现,只有改变的栏目才闪烁,也就是进行重绘,数据没有改变的栏目还是保持原样,这样就大大节省了浏览器重新渲染的开销
vue中使用
h函数生成虚拟DOM返回
const vm = new Vue({ el: '#app', data: { user: {name:'poetry'} }, render(h){ // h() // h(App) // h('div',[]) let vnode = h('div',{},'hello world'); return vnode } });@程序员poetry: 代码已经复制到剪贴板
相关源码
// src/core/vdom/create-element.js export function createElement ( // 创建元素 context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean ): VNode | Array<VNode> { if (Array.isArray(data) || isPrimitive(data)) { normalizationType = children children = data data = undefined } if (isTrue(alwaysNormalize)) { normalizationType = ALWAYS_NORMALIZE } // 创建元素 return _createElement(context, tag, data, children, normalizationType) } export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number ): VNode | Array<VNode> { if (isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV !== 'production' && warn( `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` + 'Always create fresh vnode data objects in each render!', context ) return createEmptyVNode() } // object syntax in v-bind if (isDef(data) && isDef(data.is)) { tag = data.is } if (!tag) { // 如果 h() 返回空节点 // in case of component :is set to falsy value return createEmptyVNode() } // warn against non-primitive key if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.key) && !isPrimitive(data.key) ) { if (!__WEEX__ || !('@binding' in data.key)) { warn( 'Avoid using non-primitive value as key, ' + 'use string/number value instead.', context ) } } // support single function children as default scoped slot if (Array.isArray(children) && typeof children[0] === 'function' ) { data = data || {} data.scopedSlots = { default: children[0] } children.length = 0 } if (normalizationType === ALWAYS_NORMALIZE) { // 处理儿子节点个数 children = normalizeChildren(children) } else if (normalizationType === SIMPLE_NORMALIZE) { children = simpleNormalizeChildren(children) } let vnode, ns if (typeof tag === 'string') { // 标签是字符串 let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { // platform built-in elements if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) { warn( `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`, context ) } vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component 组件的虚拟节点 vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor 组件的虚拟节点 vnode = createComponent(tag, data, context, children) } if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { if (isDef(ns)) applyNS(vnode, ns) if (isDef(data)) registerDeepBindings(data) return vnode } else { return createEmptyVNode() } } function applyNS (vnode, ns, force) { vnode.ns = ns if (vnode.tag === 'foreignObject') { // use default namespace inside foreignObject ns = undefined force = true } if (isDef(vnode.children)) { for (let i = 0, l = vnode.children.length; i < l; i++) { const child = vnode.children[i] if (isDef(child.tag) && ( isUndef(child.ns) || (isTrue(force) && child.tag !== 'svg'))) { applyNS(child, ns, force) } } } } // ref #5318 // necessary to ensure parent re-render when deep bindings like :style and // :class are used on slot nodes function registerDeepBindings (data) { if (isObject(data.style)) { traverse(data.style) } if (isObject(data.class)) { traverse(data.class) } }@程序员poetry: 代码已经复制到剪贴板
// 虚拟节点的实现 src/core/vdom/vnode.js export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node // strictly internal raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? asyncFactory: Function | void; // async component factory function asyncMeta: Object | void; isAsyncPlaceholder: boolean; ssrContext: Object | void; fnContext: Component | void; // real context vm for functional nodes fnOptions: ?ComponentOptions; // for SSR caching devtoolsMeta: ?Object; // used to store functional render context for devtools fnScopeId: ?string; // functional scope id support constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions, asyncFactory?: Function ) { this.tag = tag this.data = data this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.fnContext = undefined this.fnOptions = undefined this.fnScopeId = undefined this.key = data && data.key this.componentOptions = componentOptions this.componentInstance = undefined this.parent = undefined this.raw = false this.isStatic = false this.isRootInsert = true this.isComment = false this.isCloned = false this.isOnce = false this.asyncFactory = asyncFactory this.asyncMeta = undefined this.isAsyncPlaceholder = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } } export const createEmptyVNode = (text: string = '') => { const node = new VNode() node.text = text node.isComment = true return node } export function createTextVNode (val: string | number) { return new VNode(undefined, undefined, undefined, String(val)) } // optimized shallow clone // used for static nodes and slot nodes because they may be reused across // multiple renders, cloning them avoids errors when DOM manipulations rely // on their elm reference. export function cloneVNode (vnode: VNode): VNode { const cloned = new VNode( vnode.tag, vnode.data, // #7975 // clone children array to avoid mutating original in case of cloning // a child. vnode.children && vnode.children.slice(), vnode.text, vnode.elm, vnode.context, vnode.componentOptions, vnode.asyncFactory ) cloned.ns = vnode.ns cloned.isStatic = vnode.isStatic cloned.key = vnode.key cloned.isComment = vnode.isComment cloned.fnContext = vnode.fnContext cloned.fnOptions = vnode.fnOptions cloned.fnScopeId = vnode.fnScopeId cloned.asyncMeta = vnode.asyncMeta cloned.isCloned = true return cloned }@程序员poetry: 代码已经复制到剪贴板
# Vue中diff算法原理
DOM操作是非常昂贵的,因此我们需要尽量地减少DOM操作。这就需要找出本次DOM必须更新的节点来更新,其他的不更新,这个找出的过程,就需要应用diff算法
vue的diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式+双指针(头尾都加指针)的方式进行比较。
简单来说,Diff算法有以下过程
- 同级比较,再比较子节点(根据
key和tag标签名判断) - 先判断一方有子节点和一方没有子节点的情况(如果新的
children没有子节点,将旧的子节点移除) - 比较都有子节点的情况(核心
diff) - 递归比较子节点
- 正常
Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以Vue将Diff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。 Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅- 在创建
VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升

vue3中采用最长递增子序列来实现
diff优化
回答范例
思路
diff算法是干什么的- 它的必要性
- 它何时执行
- 具体执行方式
- 拔高:说一下
vue3中的优化
回答范例
Vue中的diff算法称为patching算法,它由Snabbdom修改而来,虚拟DOM要想转化为真实DOM就需要通过patch方法转换- 最初
Vue1.x视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOM和patching算法支持,但是这样粒度过细导致Vue1.x无法承载较大应用;Vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,此时就需要引入patching算法才能精确找到发生变化的地方并高效更新 vue中diff执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch函数,并传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作patch过程是一个递归过程,遵循深度优先、同层比较的策略;以vue3的patch为例
- 首先判断两个节点是否为相同同类节点,不同则删除重新创建
- 如果双方都是文本则更新文本内容
- 如果双方都是元素节点则递归更新子元素,同时更新元素属性
- 更新子节点时又分了几种情况
- 新的子节点是文本,老的子节点是数组则清空,并设置文本;
- 新的子节点是文本,老的子节点是文本则直接更新文本;
- 新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;
- 新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节blabla
vue3中引入的更新策略:静态节点标记等
vdom中diff算法的简易实现
以下代码只是帮助大家理解diff算法的原理和流程
- 将
vdom转化为真实dom:
const createElement = (vnode) => { let tag = vnode.tag; let attrs = vnode.attrs || {}; let children = vnode.children || []; if(!tag) { return null; } //创建元素 let elem = document.createElement(tag); //属性 let attrName; for (attrName in attrs) { if(attrs.hasOwnProperty(attrName)) { elem.setAttribute(attrName, attrs[attrName]); } } //子元素 children.forEach(childVnode => { //给elem添加子元素 elem.appendChild(createElement(childVnode)); }) //返回真实的dom元素 return elem; }@程序员poetry: 代码已经复制到剪贴板
- 用简易
diff算法做更新操作
function updateChildren(vnode, newVnode) { let children = vnode.children || []; let newChildren = newVnode.children || []; children.forEach((childVnode, index) => { let newChildVNode = newChildren[index]; if(childVnode.tag === newChildVNode.tag) { //深层次对比, 递归过程 updateChildren(childVnode, newChildVNode); } else { //替换 replaceNode(childVnode, newChildVNode); } }) }@程序员poetry: 代码已经复制到剪贴板
Vue diff相关源码
// src/core/vdom/patch.js 700 function assertNodeMatch (node, vnode, inVPre) { if (isDef(vnode.tag)) { return vnode.tag.indexOf('vue-component') === 0 || ( !isUnknownElement(vnode, inVPre) && vnode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase()) ) } else { return node.nodeType === (vnode.isComment ? 8 : 3) } } return function patch (oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { // 此为组件卸载逻辑 if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { // 此为组件挂载 // empty mount (likely as component), create new root element isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { if (isRealElement) { // 真实元素挂载 // mounting to a real element // check if this is server-rendered content and if we can perform // a successful hydration. if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } else if (process.env.NODE_ENV !== 'production') { warn( 'The client-side rendered virtual DOM tree is not matching ' + 'server-rendered content. This is likely caused by incorrect ' + 'HTML markup, for example nesting block-level elements inside ' + '<p>, or missing <tbody>. Bailing hydration and performing ' + 'full client-side render.' ) } } // either not server-rendered, or hydration failed. // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode) // 根据真实元素 产生虚拟节点 } // replacing existing element const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // 找到父亲 // create new node 创建新节点 createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // update parent placeholder node element, recursively if (isDef(vnode.parent)) { // 依次更新父占位符 let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } // #6513 // invoke insert hooks that may have been merged by create hooks. // e.g. for directives that uses the "inserted" hook. const insert = ancestor.data.hook.insert if (insert.merged) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // destroy old node if (isDef(parentElm)) { // 销毁老节点 removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } // 调用插入的钩子 -》 callInsert invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }@程序员poetry: 代码已经复制到剪贴板
// 比较两个虚拟节点 function patchVnode ( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { if (oldVnode === vnode) { return } if (isDef(vnode.elm) && isDef(ownerArray)) { // clone reused vnode vnode = ownerArray[index] = cloneVNode(vnode) } const elm = vnode.elm = oldVnode.elm // 复用老节点 if (isTrue(oldVnode.isAsyncPlaceholder)) { // 如果是异步占位符跳过 if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // reuse element for static trees. // note we only do this if the vnode is cloned - // if the new node is not cloned it means the render functions have been // reset by the hot-reload-api and we need to do a proper re-render. if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && // 都是静态节点,key相同 (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) // 是克隆节点 或者 带有once,直接复用 ) { vnode.componentInstance = oldVnode.componentInstance return } let i // 组件更新逻辑 const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { // 调用更新方法 for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } if (isUndef(vnode.text)) { // 如果不是文本节点 if (isDef(oldCh) && isDef(ch)) { // 两方都有儿子, 而且不是同一个儿子 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { // 如果只有新的有儿子 if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(ch) } if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') // 删除添加新节点 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { // 如果老得有儿子 removeVnodes(oldCh, 0, oldCh.length - 1) // 删除节点 } else if (isDef(oldVnode.text)) { // 如果老的是文本 nodeOps.setTextContent(elm, '') // 清空文本中内容 } } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text) // 文本不相同直接设置新值 } if (isDef(data)) { // 调用postpatch钩子 if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } } function invokeInsertHook (vnode, queue, initial) { // delay insert hooks for component root nodes, invoke them after the // element is really inserted if (isTrue(initial) && isDef(vnode.parent)) { vnode.parent.data.pendingInsert = queue } else { for (let i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]) } } } let hydrationBailed = false // list of modules that can skip create hook during hydration because they // are already rendered on the client or has no need for initialization // Note: style is excluded because it relies on initial clone for future // deep updates (#7063). const isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key') // Note: this is a browser-only function so we can assume elms are DOM nodes. function hydrate (elm, vnode, insertedVnodeQueue, inVPre) { let i const { tag, data, children } = vnode inVPre = inVPre || (data && data.pre) vnode.elm = elm if (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) { vnode.isAsyncPlaceholder = true return true } // assert node match if (process.env.NODE_ENV !== 'production') { if (!assertNodeMatch(elm, vnode, inVPre)) { return false } } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode, true /* hydrating */) if (isDef(i = vnode.componentInstance)) { // child component. it should have hydrated its own tree. initComponent(vnode, insertedVnodeQueue) return true } } if (isDef(tag)) { if (isDef(children)) { // empty element, allow client to pick up and populate children if (!elm.hasChildNodes()) { createChildren(vnode, children, insertedVnodeQueue) } else { // v-html and domProps: innerHTML if (isDef(i = data) && isDef(i = i.domProps) && isDef(i = i.innerHTML)) { if (i !== elm.innerHTML) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && typeof console !== 'undefined' && !hydrationBailed ) { hydrationBailed = true console.warn('Parent: ', elm) console.warn('server innerHTML: ', i) console.warn('client innerHTML: ', elm.innerHTML) } return false } } else { // iterate and compare children lists let childrenMatch = true let childNode = elm.firstChild for (let i = 0; i < children.length; i++) { if (!childNode || !hydrate(childNode, children[i], insertedVnodeQueue, inVPre)) { childrenMatch = false break } childNode = childNode.nextSibling } // if childNode is not null, it means the actual childNodes list is // longer than the virtual children list. if (!childrenMatch || childNode) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && typeof console !== 'undefined' && !hydrationBailed ) { hydrationBailed = true console.warn('Parent: ', elm) console.warn('Mismatching childNodes vs. VNodes: ', elm.childNodes, children) } return false } } } } if (isDef(data)) { let fullInvoke = false for (const key in data) { if (!isRenderedModule(key)) { fullInvoke = true invokeCreateHooks(vnode, insertedVnodeQueue) break } } if (!fullInvoke && data['class']) { // ensure collecting deps for deep class bindings for future updates traverse(data['class']) } } } else if (elm.data !== vnode.text) { elm.data = vnode.text } return true }@程序员poetry: 代码已经复制到剪贴板
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, vnodeToMove, refElm // removeOnly is a special flag used only by <transition-group> // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(newCh) } // 新老节点有一方循环完毕则patch 完毕 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // 乱序比对 if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } } if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { removeVnodes(oldCh, oldStartIdx, oldEndIdx) } }@程序员poetry: 代码已经复制到剪贴板
# 既然Vue通过数据劫持可以精准探测数据变化,为什么还需要虚拟DOM进行diff检测差异
- 响应式数据变化,
Vue确实可以在数据变化时,响应式系统可以立刻得知。但是如果给每个属性都添加watcher用于更新的话,会产生大量的watcher从而降低性能 - 而且粒度过细也得导致更新不准确的问题,所以
vue采用了组件级的watcher配合diff来检测差异
# 请说明Vue中key的作用和原理,谈谈你对它的理解

key是为Vue中的VNode标记的唯一id,在patch过程中通过key可以判断两个虚拟节点是否是相同节点,通过这个key,我们的diff操作可以更准确、更快速diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行比对,然后检出差异- 尽量不要采用索引作为
key - 如果不加
key,那么vue会选择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug - 更准确:因为带
key就不是就地复用了,在sameNode函数a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。 - 更快速:
key的唯一性可以被Map数据结构充分利用,相比于遍历查找的时间复杂度O(n),Map的时间复杂度仅仅为O(1),比遍历方式更快。
源码如下:
function createKeyToOldIdx (children, beginIdx, endIdx) { let i, key const map = {} for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key if (isDef(key)) map[key] = i } return map }@程序员poetry: 代码已经复制到剪贴板
回答范例
分析
这是一道特别常见的问题,主要考查大家对虚拟DOM和patch细节的掌握程度,能够反映面试者理解层次
思路分析:
- 给出结论,
key的作用是用于优化patch性能 key的必要性- 实际使用方式
- 总结:可从源码层面描述一下
vue如何判断两个节点是否相同
回答范例:
key的作用主要是为了更高效的更新虚拟DOMvue在patch过程中判断两个节点是否是相同节点是key是一个必要条件,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能- 实际使用中在渲染一组列表时
key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,这可能导致一些隐蔽的bug;vue中在使用相同标签元素过渡切换时,也会使用key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果 - 从源码中可以知道,
vue判断两个节点是否相同时主要判断两者的key和标签类型(如div)等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的
如果不使用
key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key是为Vue中vnode的唯一标记,通过这个key,我们的diff操作可以更准确、更快速

diff程可以概括为:
oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较,这四种比较方式就是首、尾、旧尾新头、旧头新尾
相关代码如下
// 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用 function isSameVnode(oldVnode, newVnode) { return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key; } // 根据key来创建老的儿子的index映射表 类似 {'a':0,'b':1} 代表key为'a'的节点在第一个位置 key为'b'的节点在第二个位置 function makeIndexByKey(children) { let map = {}; children.forEach((item, index) => { map[item.key] = index; }); return map; } // 生成的映射表 let map = makeIndexByKey(oldCh);@程序员poetry: 代码已经复制到剪贴板
# 17 Vue组件相关
# Vue组件为什么只能有一个根元素
vue3中没有问题
Vue.createApp({ components: { comp: { template: ` <div>root1</div> <div>root2</div> ` } } }).mount('#app')@程序员poetry: 代码已经复制到剪贴板
vue2中组件确实只能有一个根,但vue3中组件已经可以多根节点了。- 之所以需要这样是因为
vdom是一颗单根树形结构,patch方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom vue3中之所以可以写多个根节点,是因为引入了Fragment的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment节点,把多个根节点作为它的children。将来patch的时候,如果发现是一个Fragment节点,则直接遍历children创建或更新
# 谈一谈对Vue组件化的理解
- 组件化开发能大幅提高开发效率、测试性、复用性等
- 常用的组件化技术:属性、自定义事件、插槽
- 降低更新频率,只重新渲染变化的组件
- 组件的特点:高内聚、低耦合、单向数据流
# Vue组件渲染和更新过程
渲染组件时,会通过
Vue.extend方法构建子组件的构造函数,并进行实例化。最终手动调用$mount()进行挂载。更新组件时会进行patchVnode流程,核心就是diff算法

# 异步组件是什么?使用场景有哪些?
分析
因为异步路由的存在,我们使用异步组件的次数比较少,因此还是有必要两者的不同。
体验
大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们
import { defineAsyncComponent } from 'vue' // defineAsyncComponent定义异步组件,返回一个包装组件。包装组件根据加载器的状态决定渲染什么内容 const AsyncComp = defineAsyncComponent(() => { // 加载函数返回Promise return new Promise((resolve, reject) => { // ...可以从服务器加载组件 resolve(/* loaded component */) }) }) // 借助打包工具实现ES模块动态导入 const AsyncComp = defineAsyncComponent(() => import('./components/MyComponent.vue') )@程序员poetry: 代码已经复制到剪贴板
回答范例
- 在大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们。
- 我们不仅可以在路由切换时懒加载组件,还可以在页面组件中继续使用异步组件,从而实现更细的分割粒度。
- 使用异步组件最简单的方式是直接给
defineAsyncComponent指定一个loader函数,结合ES模块动态导入函数import可以快速实现。我们甚至可以指定loadingComponent和errorComponent选项从而给用户一个很好的加载反馈。另外Vue3中还可以结合Suspense组件使用异步组件。 - 异步组件容易和路由懒加载混淆,实际上不是一个东西。异步组件不能被用于定义懒加载路由上,处理它的是
vue框架,处理路由组件加载的是vue-router。但是可以在懒加载的路由组件中使用异步组件
# 为什么要使用异步组件
- 节省打包出的结果,异步组件分开打包,采用
jsonp的方式进行加载,有效解决文件过大的问题。 - 核心就是包组件定义变成一个函数,依赖
import()语法,可以实现文件的分割加载。
components:{ AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([]) }@程序员poetry: 代码已经复制到剪贴板
原理
export function ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { // async component let asyncFactory if (isUndef(Ctor.cid)) { asyncFactory = Ctor Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回 undefiend // 第二次渲染时Ctor不为undefined if (Ctor === undefined) { return createAsyncPlaceholder( // 渲染占位符 空虚拟节点 asyncFactory, data, context, children, tag ) } } } function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void { if (isDef(factory.resolved)) { // 3.在次渲染时可以拿到获取的最新组件 return factory.resolved } const resolve = once((res: Object | Class<Component>) => { factory.resolved = ensureCtor(res, baseCtor) if (!sync) { forceRender(true) //2. 强制更新视图重新渲染 } else { owners.length = 0 } }) const reject = once(reason => { if (isDef(factory.errorComp)) { factory.error = true forceRender(true) } }) const res = factory(resolve, reject)// 1.将resolve方法和reject方法传入,用户调用 resolve方法后 sync = false return factory.resolved }@程序员poetry: 代码已经复制到剪贴板
# 函数式组件优势和原理
函数组件的特点
- 函数式组件需要在声明组件是指定
functional:true - 不需要实例化,所以没有
this,this通过render函数的第二个参数context来代替 - 没有生命周期钩子函数,不能使用计算属性,
watch - 不能通过
$emit对外暴露事件,调用事件只能通过context.listeners.click的方式调用外部传入的事件 - 因为函数式组件是没有实例化的,所以在外部通过
ref去引用组件时,实际引用的是HTMLElement - 函数式组件的
props可以不用显示声明,所以没有在props里面声明的属性都会被自动隐式解析为prop,而普通组件所有未声明的属性都解析到$attrs里面,并自动挂载到组件根元素上面(可以通过inheritAttrs属性禁止)
优点
- 由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件
- 函数式组件结构比较简单,代码结构更清晰
使用场景:
- 一个简单的展示组件,作为容器组件使用 比如
router-view就是一个函数式组件 - “高阶组件”——用于接收一个组件作为参数,返回一个被包装过的组件
例子
Vue.component('functional',{ // 构造函数产生虚拟节点的 functional:true, // 函数式组件 // data={attrs:{}} render(h){ return h('div','test') } }) const vm = new Vue({ el: '#app' })@程序员poetry: 代码已经复制到剪贴板
源码相关
// functional component if (isTrue(Ctor.options.functional)) { // 带有functional的属性的就是函数式组件 return createFunctionalComponent(Ctor, propsData, data, context, children) } // extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners const listeners = data.on // 处理事件 // replace with listeners with .native modifier // so it gets processed during parent component patch. data.on = data.nativeOn // 处理原生事件 // install component management hooks onto the placeholder node installComponentHooks(data) // 安装组件相关钩子 (函数式组件没有调用此方法,从而性能高于普通组件)@程序员poetry: 代码已经复制到剪贴板
# Vue组件之间通信方式有哪些
Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信:
父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信
组件传参的各种方式

组件通信常用方式有以下几种
props / $emit适用 父子组件通信- 父组件向子组件传递数据是通过
prop传递的,子组件传递数据给父组件是通过$emit触发事件来做到的
- 父组件向子组件传递数据是通过
ref与$parent / $children(vue3废弃)适用 父子组件通信ref:如果在普通的DOM元素上使用,引用指向的就是DOM元素;如果用在子组件上,引用就指向组件实例$parent / $children:访问访问父组件的属性或方法 / 访问子组件的属性或方法
EventBus ($emit / $on)适用于 父子、隔代、兄弟组件通信- 这种方法通过一个空的
Vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件
- 这种方法通过一个空的
$attrs / $listeners(vue3废弃)适用于 隔代组件通信$attrs:包含了父作用域中不被prop所识别 (且获取) 的特性绑定 (class和style除外 )。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定 (class和style除外 ),并且可以通过v-bind="$attrs"传入内部组件。通常配合inheritAttrs选项一起使用$listeners:包含了父作用域中的 (不含.native修饰器的)v-on事件监听器。它可以通过v-on="$listeners"传入内部组件
provide / inject适用于 隔代组件通信- 祖先组件中通过
provider来提供变量,然后在子孙组件中通过inject来注入变量。provide / injectAPI 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系
- 祖先组件中通过
$root适用于 隔代组件通信 访问根组件中的属性或方法,是根组件,不是父组件。$root只对根组件有用Vuex适用于 父子、隔代、兄弟组件通信Vuex是一个专为Vue.js应用程序开发的状态管理模式。每一个Vuex应用的核心就是store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 (state)Vuex的状态存储是响应式的。当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会相应地得到高效更新。- 改变
store中的状态的唯一途径就是显式地提交 (commit)mutation。这样使得我们可以方便地跟踪每一个状态的变化。
根据组件之间关系讨论组件通信最为清晰有效
- 父子组件:
props/$emit/$parent/ref - 兄弟组件:
$parent/eventbus/vuex - 跨层级关系:
eventbus/vuex/provide+inject/$attrs + $listeners/$root
下面演示组件之间通讯三种情况: 父传子、子传父、兄弟组件之间的通讯
1. 父子组件通信
使用
props,父组件可以使用props向子组件传递数据。
父组件vue模板father.vue:
<template> <child :msg="message"></child> </template> <script> import child from './child.vue'; export default { components: { child }, data () { return { message: 'father message'; } } } </script>@程序员poetry: 代码已经复制到剪贴板
子组件vue模板child.vue:
<template> <div>{{msg}}</div> </template> <script> export default { props: { msg: { type: String, required: true } } } </script>@程序员poetry: 代码已经复制到剪贴板
回调函数(callBack)
父传子:将父组件里定义的method作为props传入子组件
// 父组件Parent.vue: <Child :changeMsgFn="changeMessage"> methods: { changeMessage(){ this.message = 'test' } }@程序员poetry: 代码已经复制到剪贴板
// 子组件Child.vue: <button @click="changeMsgFn"> props:['changeMsgFn']@程序员poetry: 代码已经复制到剪贴板
子组件向父组件通信
父组件向子组件传递事件方法,子组件通过
$emit触发事件,回调给父组件
父组件vue模板father.vue:
<template> <child @msgFunc="func"></child> </template> <script> import child from './child.vue'; export default { components: { child }, methods: { func (msg) { console.log(msg); } } } </script>@程序员poetry: 代码已经复制到剪贴板
子组件vue模板child.vue:
<template> <button @click="handleClick">点我</button> </template> <script> export default { props: { msg: { type: String, required: true } }, methods () { handleClick () { //........ this.$emit('msgFunc'); } } } </script>@程序员poetry: 代码已经复制到剪贴板
2. provide / inject 跨级访问祖先组件的数据
父组件通过使用provide(){return{}}提供需要传递的数据
export default { data() { return { title: '我是父组件', name: 'poetry' } }, methods: { say() { alert(1) } }, // provide属性 能够为后面的后代组件/嵌套的组件提供所需要的变量和方法 provide() { return { message: '我是祖先组件提供的数据', name: this.name, // 传递属性 say: this.say } } }@程序员poetry: 代码已经复制到剪贴板
子组件通过使用inject:[“参数1”,”参数2”,…]接收父组件传递的参数
<template> <p>曾孙组件</p> <p>{{message}}</p> </template> <script> export default { // inject 注入/接收祖先组件传递的所需要的数据即可 //接收到的数据 变量 跟data里面的变量一样 可以直接绑定到页面 {{}} inject: [ "message","say"], mounted() { this.say(); }, }; </script>@程序员poetry: 代码已经复制到剪贴板
3. $parent + $children 获取父组件实例和子组件实例的集合
this.$parent可以直接访问该组件的父实例或组件- 父组件也可以通过
this.$children访问它所有的子组件;需要注意$children并不保证顺序,也不是响应式的
<!-- parent.vue --> <template> <div> <child1></child1> <child2></child2> <button @click="clickChild">$children方式获取子组件值</button> </div> </template> <script> import child1 from './child1' import child2 from './child2' export default { data(){ return { total: 108 } }, components: { child1, child2 }, methods: { funa(e){ console.log("index",e) }, clickChild(){ console.log(this.$children[0].msg); console.log(this.$children[1].msg); } } } </script>@程序员poetry: 代码已经复制到剪贴板
<!-- child1.vue --> <template> <div> <button @click="parentClick">点击访问父组件</button> </div> </template> <script> export default { data(){ return { msg:"child1" } }, methods: { // 访问父组件数据 parentClick(){ this.$parent.funa("xx") console.log(this.$parent.total); } } } </script>@程序员poetry: 代码已经复制到剪贴板
<!-- child2.vue --> <template> <div> child2 </div> </template> <script> export default { data(){ return { msg: 'child2' } } } </script>@程序员poetry: 代码已经复制到剪贴板
4. $attrs + $listeners多级组件通信
$attrs包含了从父组件传过来的所有props属性
// 父组件Parent.vue: <Child :name="name" :age="age"/> // 子组件Child.vue: <GrandChild v-bind="$attrs" /> // 孙子组件GrandChild <p>姓名:{{$attrs.name}}</p> <p>年龄:{{$attrs.age}}</p>@程序员poetry: 代码已经复制到剪贴板
$listeners包含了父组件监听的所有事件
// 父组件Parent.vue: <Child :name="name" :age="age" @changeNameFn="changeName"/> // 子组件Child.vue: <button @click="$listeners.changeNameFn"></button>@程序员poetry: 代码已经复制到剪贴板
5. ref 父子组件通信
// 父组件Parent.vue: <Child ref="childComp"/> <button @click="changeName"></button> changeName(){ console.log(this.$refs.childComp.age); this.$refs.childComp.changeAge() } // 子组件Child.vue: data(){ return{ age:20 } }, methods(){ changeAge(){ this.age=15 } }@程序员poetry: 代码已经复制到剪贴板
6. 非父子, 兄弟组件之间通信
vue2中废弃了broadcast广播和分发事件的方法。父子组件中可以用props和$emit()。如何实现非父子组件间的通信,可以通过实例一个vue实例Bus作为媒介,要相互通信的兄弟组件之中,都引入Bus,然后通过分别调用Bus事件触发和监听来实现通信和参数传递。Bus.js可以是这样:
import Vue from 'vue' export default new Vue()@程序员poetry: 代码已经复制到剪贴板
在需要通信的组件都引入Bus.js:
<template> <button @click="toBus">子组件传给兄弟组件</button> </template> <script> import Bus from '../common/js/bus.js' export default{ methods: { toBus () { Bus.$emit('on', '来自兄弟组件') } } } </script>@程序员poetry: 代码已经复制到剪贴板
另一个组件也import Bus.js 在钩子函数中监听on事件
import Bus from '../common/js/bus.js' export default { data() { return { message: '' } }, mounted() { Bus.$on('on', (msg) => { this.message = msg }) } }@程序员poetry: 代码已经复制到剪贴板
7. $root 访问根组件中的属性或方法
- 作用:访问根组件中的属性或方法
- 注意:是根组件,不是父组件。
$root只对根组件有用
var vm = new Vue({ el: "#app", data() { return { rootInfo:"我是根元素的属性" } }, methods: { alerts() { alert(111) } }, components: { com1: { data() { return { info: "组件1" } }, template: "<p>{{ info }} <com2></com2></p>", components: { com2: { template: "<p>我是组件1的子组件</p>", created() { this.$root.alerts()// 根组件方法 console.log(this.$root.rootInfo)// 我是根元素的属性 } } } } } });@程序员poetry: 代码已经复制到剪贴板
# 组件中写name属性的好处
可以标识组件的具体名称方便调试和查找对应属性
// 源码位置 src/core/global-api/extend.js // enable recursive self-lookup if (name) { Sub.options.components[name] = Sub // 记录自己 在组件中递归自己 -> jsx }@程序员poetry: 代码已经复制到剪贴板
# Vue.extend 作用和原理
官方解释:
Vue.extend使用基础Vue构造器,创建一个“子类”。参数是一个包含组件选项的对象。
其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并
extend是构造一个组件的语法器。然后这个组件你可以作用到Vue.component这个全局注册方法里还可以在任意vue模板里使用组件。 也可以作用到vue实例或者某个组件中的components属性中并在内部使用apple组件。Vue.component你可以创建 ,也可以取组件。
相关代码如下
export default function initExtend(Vue) { let cid = 0; //组件的唯一标识 // 创建子类继承Vue父类 便于属性扩展 Vue.extend = function (extendOptions) { // 创建子类的构造函数 并且调用初始化方法 const Sub = function VueComponent(options) { this._init(options); //调用Vue初始化方法 }; Sub.cid = cid++; Sub.prototype = Object.create(this.prototype); // 子类原型指向父类 Sub.prototype.constructor = Sub; //constructor指向自己 Sub.options = mergeOptions(this.options, extendOptions); //合并自己的options和父类的options return Sub; }; }@程序员poetry: 代码已经复制到剪贴板
# Vue中如何扩展一个组件
此题属于实践题,考察大家对vue常用api使用熟练度,答题时不仅要列出这些解决方案,同时最好说出他们异同
答题思路:
- 按照逻辑扩展和内容扩展来列举
- 逻辑扩展有:
mixins、extends、composition api - 内容扩展有
slots;
- 逻辑扩展有:
- 分别说出他们使用方法、场景差异和问题。
- 作为扩展,还可以说说
vue3中新引入的composition api带来的变化
回答范例:
- 常见的组件扩展方法有:
mixins,slots,extends等 - 混入
mixins是分发Vue组件中可复用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项
// 复用代码:它是一个配置对象,选项和组件里面一样 const mymixin = { methods: { dosomething(){} } } // 全局混入:将混入对象传入 Vue.mixin(mymixin) // 局部混入:做数组项设置到mixins选项,仅作用于当前组件 const Comp = { mixins: [mymixin] }@程序员poetry: 代码已经复制到剪贴板
- 插槽主要用于
vue组件中的内容分发,也可以用于组件扩展
子组件Child
<div> <slot>这个内容会被父组件传递的内容替换</slot> </div>@程序员poetry: 代码已经复制到剪贴板
父组件Parent
<div> <Child>来自父组件内容</Child> </div>@程序员poetry: 代码已经复制到剪贴板
如果要精确分发到不同位置可以使用具名插槽,如果要使用子组件中的数据可以使用作用域插槽
- 组件选项中还有一个不太常用的选项
extends,也可以起到扩展组件的目的
// 扩展对象 const myextends = { methods: { dosomething(){} } } // 组件扩展:做数组项设置到extends选项,仅作用于当前组件 // 跟混入的不同是它只能扩展单个对象 // 另外如果和混入发生冲突,该选项优先级较高,优先起作用 const Comp = { extends: myextends }@程序员poetry: 代码已经复制到剪贴板
- 混入的数据和方法不能明确判断来源且可能和当前组件内变量产生命名冲突,
vue3中引入的composition api,可以很好解决这些问题,利用独立出来的响应式模块可以很方便的编写独立逻辑并提供响应式的数据,然后在setup选项中组合使用,增强代码的可读性和维护性。例如
// 复用逻辑1 function useXX() {} // 复用逻辑2 function useYY() {} // 逻辑组合 const Comp = { setup() { const {xx} = useXX() const {yy} = useYY() return {xx, yy} } }@程序员poetry: 代码已经复制到剪贴板
# 子组件可以直接改变父组件的数据么,说明原因
这是一个实践知识点,组件化开发过程中有个单项数据流原则,不在子组件中修改父组件是个常识问题
思路
- 讲讲单项数据流原则,表明为何不能这么做
- 举几个常见场景的例子说说解决方案
- 结合实践讲讲如果需要修改父组件状态应该如何做
回答范例
- 所有的
prop都使得其父子之间形成了一个单向下行绑定:父级prop的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。另外,每次父级组件发生变更时,子组件中所有的prop都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变prop。如果你这样做了,Vue会在浏览器控制台中发出警告
const props = defineProps(['foo']) // ❌ 下面行为会被警告, props是只读的! props.foo = 'bar'@程序员poetry: 代码已经复制到剪贴板
- 实际开发过程中有两个场景会想要修改一个属性:
这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:
const props = defineProps(['initialCounter']) const counter = ref(props.initialCounter)@程序员poetry: 代码已经复制到剪贴板
这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop 的值来定义一个计算属性:
const props = defineProps(['size']) // prop变化,计算属性自动更新 const normalizedSize = computed(() => props.size.trim().toLowerCase())@程序员poetry: 代码已经复制到剪贴板
- 实践中如果确实想要改变父组件属性应该
emit一个事件让父组件去做这个变更。注意虽然我们不能直接修改一个传入的对象或者数组类型的prop,但是我们还是能够直接改内嵌的对象或属性
# 什么是递归组件?举个例子说明下?
分析
递归组件我们用的比较少,但是在Tree、Menu这类组件中会被用到。
体验
组件通过组件名称引用它自己,这种情况就是递归组件
<template> <li> <div> {{ model.name }}</div> <ul v-show="isOpen" v-if="isFolder"> <!-- 注意这里:组件递归渲染了它自己 --> <TreeItem class="item" v-for="model in model.children" :model="model"> </TreeItem> </ul> </li> <script> export default { name: 'TreeItem', // ... } </script>@程序员poetry: 代码已经复制到剪贴板
回答范例
- 如果某个组件通过组件名称引用它自己,这种情况就是递归组件。
- 实际开发中类似
Tree、Menu这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。 - 使用递归组件时,由于我们并未也不能在组件内部导入它自己,所以设置组件
name属性,用来查找组件定义,如果使用SFC,则可以通过SFC文件名推断。组件内部通常也要有递归结束条件,比如model.children这样的判断。 - 查看生成渲染函数可知,递归组件查找时会传递一个布尔值给
resolveComponent,这样实际获取的组件就是当前组件本身
原理
递归组件编译结果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)
const _component_Comp = _resolveComponent("Comp", true)@程序员poetry: 代码已经复制到剪贴板
就是在传递maybeSelfReference
export function resolveComponent( name: string, maybeSelfReference?: boolean ): ConcreteComponent | string { return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name }@程序员poetry: 代码已经复制到剪贴板
resolveAsset中最终返回的是组件自身:
if (!res && maybeSelfReference) { // fallback to implicit self-reference return Component }@程序员poetry: 代码已经复制到剪贴板
# 18 为什么Vue采用异步渲染
Vue 是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能, Vue 会在本轮数据更新后,在异步更新视图。核心思想
nextTick

源码相关
dep.notify()通知watcher进行更新,subs[i].update依次调用watcher的update,queueWatcher将watcher去重放入队列,nextTick(flushSchedulerQueue)在下一tick中刷新watcher队列(异步)
update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this); // 当数据发生变化时会将watcher放到一个队列中批量更新 } } export function queueWatcher (watcher: Watcher) { const id = watcher.id // 会对相同的watcher进行过滤 if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true if (process.env.NODE_ENV !== 'production' && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) // 调用nextTick方法 批量的进行更新 } } }@程序员poetry: 代码已经复制到剪贴板
# 19 v-if和v-show区别
v-if是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建,如果条件不成立不会渲染当前指令所在节点的dom元素v-show:不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的display的block或none属性进行切换
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景
原理
const VueTemplateCompiler = require('vue-template-compiler'); let r1 = VueTemplateCompiler.compile(`<div v-if="true"><span v-for="i in 3">hello</span></div>`); // with(this) { // return (true) ? _c('div', _l((3), function (i) { // return _c('span', [_v("hello")]) // }), 0) : _e() // }@程序员poetry: 代码已经复制到剪贴板
const VueTemplateCompiler = require('vue-template-compiler'); let r2 = VueTemplateCompiler.compile(`<div v-show="true"></div>`); /** with(this) { * return _c('div', { * directives: [{ name: "show", rawName: "v-show", value: (true), expression: "true" }] * }) * }*/ // v-show 操作的是样式 定义在platforms/web/runtime/directives/show.js bind (el: any, { value }: VNodeDirective, vnode: VNodeWithData) { vnode = locateNode(vnode) const transition = vnode.data && vnode.data.transition const originalDisplay = el.__vOriginalDisplay = el.style.display === 'none' ? '' : el.style.display if (value && transition) { vnode.data.show = true enter(vnode, () => { el.style.display = originalDisplay }) } else { el.style.display = value ? originalDisplay : 'none' } }@程序员poetry: 代码已经复制到剪贴板
# 20 v-if和v-for哪个优先级更高
- 实践中不应该把
v-for和v-if放一起 - 在
vue2中,v-for的优先级是高于v-if,把它们放在一起,输出的渲染函数中可以看出会先执行循环再判断条件,哪怕我们只渲染列表中一小部分元素,也得在每次重渲染的时候遍历整个列表,这会比较浪费;另外需要注意的是在vue3中则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量还不存在,就会导致异常 - 通常有两种情况下导致我们这样做:
- 为了过滤列表中的项目 (比如
v-for="user in users" v-if="user.isActive")。此时定义一个计算属性 (比如activeUsers),让其返回过滤后的列表即可(比如users.filter(u=>u.isActive)) - 为了避免渲染本应该被隐藏的列表 (比如
v-for="user in users" v-if="shouldShowUsers")。此时把v-if移动至容器元素上 (比如ul、ol)或者外面包一层template即可
- 为了过滤列表中的项目 (比如
- 文档中明确指出永远不要把
v-if和v-for同时用在同一个元素上,显然这是一个重要的注意事项 - 源码里面关于代码生成的部分,能够清晰的看到是先处理
v-if还是v-for,顺序上vue2和vue3正好相反,因此产生了一些症状的不同,但是不管怎样都是不能把它们写在一起的
# 21 Vue的事件绑定原理
原生事件绑定是通过
addEventListener绑定给真实元素的,组件事件绑定是通过Vue自定义的$on实现的。如果要在组件上使用原生事件,需要加.native修饰符,这样就相当于在父组件中把子组件当做普通html标签,然后加上原生事件。
$on、$emit 是基于发布订阅模式的,维护一个事件中心,on 的时候将事件按名称存在事件中心里,称之为订阅者,然后 emit 将对应的事件进行发布,去执行事件中心里的对应的监听器
EventEmitter(发布订阅模式--简单版)
// 手写发布订阅模式 EventEmitter class EventEmitter { constructor() { this.events = {}; } // 实现订阅 on(type, callBack) { if (!this.events) this.events = Object.create(null); if (!this.events[type]) { this.events[type] = [callBack]; } else { this.events[type].push(callBack); } } // 删除订阅 off(type, callBack) { if (!this.events[type]) return; this.events[type] = this.events[type].filter(item => { return item !== callBack; }); } // 只执行一次订阅事件 once(type, callBack) { function fn() { callBack(); this.off(type, fn); } this.on(type, fn); } // 触发事件 emit(type, ...rest) { this.events[type] && this.events[type].forEach(fn => fn.apply(this, rest)); } } // 使用如下 const event = new EventEmitter(); const handle = (...rest) => { console.log(rest); }; event.on("click", handle); event.emit("click", 1, 2, 3, 4); event.off("click", handle); event.emit("click", 1, 2); event.once("dbClick", () => { console.log(123456); }); event.emit("dbClick"); event.emit("dbClick");@程序员poetry: 代码已经复制到剪贴板
源码分析

- 原生 dom 的绑定
Vue在创建真是dom时会调用createElm,默认会调用invokeCreateHooks- 会遍历当前平台下相对的属性处理代码,其中就有
updateDOMListeners方法,内部会传入add方法
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) { if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) { return } const on = vnode.data.on || {} const oldOn = oldVnode.data.on || {} target = vnode.elm normalizeEvents(on) updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context) target = undefined } function add ( name: string, handler: Function, capture: boolean, passive: boolean ) { target.addEventListener( // 给当前的dom添加事件 name, handler, supportsPassive ? { capture, passive } : capture ) }@程序员poetry: 代码已经复制到剪贴板
vue中绑定事件是直接绑定给真实dom元素的
- 组件中绑定事件
export function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object ) { target = vm updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm) target = undefined } function add (event, fn) { target.$on(event, fn) }@程序员poetry: 代码已经复制到剪贴板
组件绑定事件是通过
vue中自定义的$on方法来实现的
# 22 Vue 是如何实现数据双向绑定的
Vue 数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:

- 输入框内容变化时,
Data中的数据同步变化。即View => Data的变化。 Data中的数据变化时,文本节点的内容同步变化。即Data => View的变化
Vue 主要通过以下 4 个步骤来实现数据双向绑定的
- 实现一个监听器 Observer:对数据对象进行遍历,包括子属性对象的属性,利用
Object.defineProperty()对属性都加上setter和getter。这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化 - 实现一个解析器 Compile:解析
Vue模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新 - 实现一个订阅者 Watcher:
Watcher订阅者是Observer和Compile之间通信的桥梁 ,主要的任务是订阅Observer中的属性值变化的消息,当收到属性值变化的消息时,触发解析器Compile中对应的更新函数 - 实现一个订阅器 Dep:订阅器采用 发布-订阅 设计模式,用来收集订阅者
Watcher,对监听器Observer和 订阅者Watcher进行统一管理

Vue 数据双向绑定原理图

# 23 v-model双向绑定原理
# v-model实现原理
我们在
vue项目中主要使用v-model指令在表单input、textarea、select等元素上创建双向数据绑定,我们知道v-model本质上不过是语法糖(可以看成是value + input方法的语法糖),v-model在内部为不同的输入元素使用不同的属性并抛出不同的事件:
text和textarea元素使用value属性和input事件checkbox和radio使用checked属性和change事件select字段将value作为prop并将change作为事件
所以我们可以v-model进行如下改写:
<input v-model="sth" /> <!-- 等同于 --> <input :value="sth" @input="sth = $event.target.value" />@程序员poetry: 代码已经复制到剪贴板
当在
input元素中使用v-model实现双数据绑定,其实就是在输入的时候触发元素的input事件,通过这个语法糖,实现了数据的双向绑定
- 这个语法糖必须是固定的,也就是说属性必须为
value,方法名必须为:input。 - 知道了
v-model的原理,我们可以在自定义组件上实现v-model
//Parent <template> {{num}} <Child v-model="num"> </template> export default { data(){ return { num: 0 } } } //Child <template> <div @click="add">Add</div> </template> export default { props: ['value'], // 属性必须为value methods:{ add(){ // 方法名为input this.$emit('input', this.value + 1) } } }@程序员poetry: 代码已经复制到剪贴板
原理
会将组件的 v-model 默认转化成value+input
const VueTemplateCompiler = require('vue-template-compiler'); const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></el- checkbox>'); // 观察输出的渲染函数: // with(this) { // return _c('el-checkbox', { // model: { // value: (check), // callback: function ($$v) { check = $$v }, // expression: "check" // } // }) // }@程序员poetry: 代码已经复制到剪贴板
// 源码位置 core/vdom/create-component.js line:155 function transformModel (options, data: any) { const prop = (options.model && options.model.prop) || 'value' const event = (options.model && options.model.event) || 'input' ;(data.attrs || (data.attrs = {}))[prop] = data.model.value const on = data.on || (data.on = {}) const existing = on[event] const callback = data.model.callback if (isDef(existing)) { if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) { on[event] = [callback].concat(existing) } } else { on[event] = callback } }@程序员poetry: 代码已经复制到剪贴板
原生的 v-model,会根据标签的不同生成不同的事件和属性
const VueTemplateCompiler = require('vue-template-compiler'); const ele = VueTemplateCompiler.compile('<input v-model="value"/>'); // with(this) { // return _c('input', { // directives: [{ name: "model", rawName: "v-model", value: (value), expression: "value" }], // domProps: { "value": (value) }, // on: {"input": function ($event) { // if ($event.target.composing) return; // value = $event.target.value // } // } // }) // }@程序员poetry: 代码已经复制到剪贴板
编译时:不同的标签解析出的内容不一样
platforms/web/compiler/directives/model.js
if (el.component) { genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime return false } else if (tag === 'select') { genSelect(el, value, modifiers) } else if (tag === 'input' && type === 'checkbox') { genCheckboxModel(el, value, modifiers) } else if (tag === 'input' && type === 'radio') { genRadioModel(el, value, modifiers) } else if (tag === 'input' || tag === 'textarea') { genDefaultModel(el, value, modifiers) } else if (!config.isReservedTag(tag)) { genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime return false }@程序员poetry: 代码已经复制到剪贴板
运行时:会对元素处理一些关于输入法的问题
platforms/web/runtime/directives/model.js
inserted (el, binding, vnode, oldVnode) { if (vnode.tag === 'select') { // #6903 if (oldVnode.elm && !oldVnode.elm._vOptions) { mergeVNodeHook(vnode, 'postpatch', () => { directive.componentUpdated(el, binding, vnode) }) } else { setSelected(el, binding, vnode.context) } el._vOptions = [].map.call(el.options, getValue) } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) { el._vModifiers = binding.modifiers if (!binding.modifiers.lazy) { el.addEventListener('compositionstart', onCompositionStart) el.addEventListener('compositionend', onCompositionEnd) // Safari < 10.2 & UIWebView doesn't fire compositionend when // switching focus before confirming composition choice // this also fixes the issue where some browsers e.g. iOS Chrome // fires "change" instead of "input" on autocomplete. el.addEventListener('change', onCompositionEnd) /* istanbul ignore if */ if (isIE9) { el.vmodel = true } } } }@程序员poetry: 代码已经复制到剪贴板
# Vue中修饰符.sync与v-model的区别
sync的作用
.sync修饰符可以实现父子组件之间的双向绑定,并且可以实现子组件同步修改父组件的值,相比较与v-model来说,sync修饰符就简单很多了- 一个组件上可以有多个
.sync修饰符
<!-- 正常父传子 --> <Son :a="num" :b="num2" /> <!-- 加上sync之后的父传子 --> <Son :a.sync="num" :b.sync="num2" /> <!-- 它等价于 --> <Son :a="num" :b="num2" @update:a="val=>num=val" @update:b="val=>num2=val" /> <!-- 相当于多了一个事件监听,事件名是update:a, --> <!-- 回调函数中,会把接收到的值赋值给属性绑定的数据项中。 -->@程序员poetry: 代码已经复制到剪贴板

v-model的工作原理
<com1 v-model="num"></com1> <!-- 等价于 --> <com1 :value="num" @input="(val)=>num=val"></com1>@程序员poetry: 代码已经复制到剪贴板
- 相同点
- 都是语法糖,都可以实现父子组件中的数据的双向通信
- 区别点
- 格式不同:
v-model="num",:num.sync="num" v-model:@input + value:num.sync:@update:numv-model只能用一次;.sync可以有多个
- 格式不同:
# 24 什么是作用域插槽
插槽
- 创建组件虚拟节点时,会将组件儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿子进行分类
{a:[vnode],b[vnode]} - 渲染组件时会拿对应的
slot属性的节点进行替换操作。(插槽的作用域为父组件)
<app> <div slot="a">xxxx</div> <div slot="b">xxxx</div> </app> slot name="a" slot name="b"@程序员poetry: 代码已经复制到剪贴板
作用域插槽
- 作用域插槽在解析的时候不会作为组件的孩子节点。会解析成函数,当子组件渲染时,会调用此函数进行渲染。(插槽的作用域为子组件)
- 普通插槽渲染的作用域是父组件,作用域插槽的渲染作用域是当前子组件。

// 插槽 const VueTemplateCompiler = require('vue-template-compiler'); let ele = VueTemplateCompiler.compile(` <my-component> <div slot="header">node</div> <div>react</div> <div slot="footer">vue</div> </my-component> ` ) // with(this) { // return _c('my-component', [_c('div', { // attrs: { "slot": "header" }, // slot: "header" // }, [_v("node")] // _文本及诶点 ) // , _v(" "), // _c('div', [_v("react")]), _v(" "), _c('div', { // attrs: { "slot": "footer" }, // slot: "footer" }, [_v("vue")])]) // } const VueTemplateCompiler = require('vue-template-compiler'); let ele = VueTemplateCompiler.compile(` <div> <slot name="header"></slot> <slot name="footer"></slot> <slot></slot> </div> ` ); with(this) { return _c('div', [_v("node"), _v(" "), _t(_v("vue")])]), _v(" "), _t("default")], 2) } // _t定义在 core/instance/render-helpers/index.js@程序员poetry: 代码已经复制到剪贴板
// 作用域插槽: let ele = VueTemplateCompiler.compile(` <app> <div slot-scope="msg" slot="footer">{{msg.a}}</div> </app> ` ); // with(this) { // return _c('app', { scopedSlots: _u([{ // // 作用域插槽的内容会被渲染成一个函数 // key: "footer", // fn: function (msg) { // return _c('div', {}, [_v(_s(msg.a))]) } }]) // }) // } // } const VueTemplateCompiler = require('vue-template-compiler'); VueTemplateCompiler.compile(` <div><slot name="footer" a="1" b="2"></slot> </div> `); // with(this) { return _c('div', [_t("footer", null, { "a": "1", "b": "2" })], 2) }@程序员poetry: 代码已经复制到剪贴板
# 25 keep-alive原理
# keep-alive 使用场景和原理
keep-alive是Vue内置的一个组件,可以实现组件缓存,当组件切换时不会对当前组件进行卸载。一般结合路由和动态组件一起使用,用于缓存组件- 提供
include和exclude属性,允许组件有条件的进行缓存。两者都支持字符串或正则表达式,include表示只有名称匹配的组件会被缓存,exclude表示任何名称匹配的组件都不会被缓存 ,其中exclude的优先级比include高 - 对应两个钩子函数
activated和deactivated,当组件被激活时,触发钩子函数activated,当组件被移除时,触发钩子函数deactivated keep-alive的中还运用了LRU(最近最少使用) 算法,选择最近最久未使用的组件予以淘汰
<keep-alive></keep-alive>包裹动态组件时,会缓存不活动的组件实例,主要用于保留组件状态或避免重新渲染- 比如有一个列表和一个详情,那么用户就会经常执行打开详情=>返回列表=>打开详情…这样的话列表和详情都是一个频率很高的页面,那么就可以对列表组件使用
<keep-alive></keep-alive>进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染
相关代码
export default { name: "keep-alive", abstract: true, //抽象组件 props: { include: patternTypes, //要缓存的组件 exclude: patternTypes, //要排除的组件 max: [String, Number], //最大缓存数 }, created() { this.cache = Object.create(null); //缓存对象 {a:vNode,b:vNode} this.keys = []; //缓存组件的key集合 [a,b] }, destroyed() { for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys); } }, mounted() { //动态监听include exclude this.$watch("include", (val) => { pruneCache(this, (name) => matches(val, name)); }); this.$watch("exclude", (val) => { pruneCache(this, (name) => !matches(val, name)); }); }, render() { const slot = this.$slots.default; //获取包裹的插槽默认值 const vnode: VNode = getFirstComponentChild(slot); //获取第一个子组件 const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions; if (componentOptions) { // check pattern const name: ?string = getComponentName(componentOptions); const { include, exclude } = this; // 不走缓存 if ( // not included 不包含 (include && (!name || !matches(include, name))) || // excluded 排除里面 (exclude && name && matches(exclude, name)) ) { //返回虚拟节点 return vnode; } const { cache, keys } = this; const key: ?string = vnode.key == null ? // same constructor may get registered as different local components // so cid alone is not enough (#3269) componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : "") : vnode.key; if (cache[key]) { //通过key 找到缓存 获取实例 vnode.componentInstance = cache[key].componentInstance; // make current key freshest remove(keys, key); //通过LRU算法把数组里面的key删掉 keys.push(key); //把它放在数组末尾 } else { cache[key] = vnode; //没找到就换存下来 keys.push(key); //把它放在数组末尾 // prune oldest entry //如果超过最大值就把数组第0项删掉 if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode); } } vnode.data.keepAlive = true; //标记虚拟节点已经被缓存 } // 返回虚拟节点 return vnode || (slot && slot[0]); }, };@程序员poetry: 代码已经复制到剪贴板
扩展补充:LRU 算法是什么?

LRU的核心思想是如果数据最近被访问过,那么将来被访问的几率也更高,所以我们将命中缓存的组件key重新插入到this.keys的尾部,这样一来,this.keys中越往头部的数据即将来被访问几率越低,所以当缓存数量达到最大值时,我们就删除将来被访问几率最低的数据,即this.keys中第一个缓存的组件
# 怎么缓存当前的组件?缓存后怎么更新
缓存组件使用keep-alive组件,这是一个非常常见且有用的优化手段,vue3中keep-alive有比较大的更新,能说的点比较多
思路
- 缓存用
keep-alive,它的作用与用法 - 使用细节,例如缓存指定/排除、结合
router和transition - 组件缓存后更新可以利用
activated或者beforeRouteEnter - 原理阐述
回答范例
- 开发中缓存组件使用
keep-alive组件,keep-alive是vue内置组件,keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM
<keep-alive> <component :is="view"></component> </keep-alive>@程序员poetry: 代码已经复制到剪贴板
- 结合属性
include和exclude可以明确指定缓存哪些组件或排除缓存指定组件。vue3中结合vue-router时变化较大,之前是keep-alive包裹router-view,现在需要反过来用router-view包裹keep-alive
<router-view v-slot="{ Component }"> <keep-alive> <component :is="Component"></component> </keep-alive> </router-view>@程序员poetry: 代码已经复制到剪贴板
- 缓存后如果要获取数据,解决方案可以有以下两种
beforeRouteEnter:在有vue-router的项目,每次进入路由的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){ next(vm=>{ console.log(vm) // 每次进入路由执行 vm.getData() // 获取数据 }) },@程序员poetry: 代码已经复制到剪贴板
actived:在keep-alive缓存的组件被激活的时候,都会执行actived钩子
activated(){ this.getData() // 获取数据 },@程序员poetry: 代码已经复制到剪贴板
keep-alive是一个通用组件,它内部定义了一个map,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component组件对应组件的vnode,如果该组件在map中存在就直接返回它。由于component的is属性是个响应式数据,因此只要它变化,keep-alive的render函数就会重新执行
# 26 Vue路由相关
# Vue-router基本使用
mode
hashhistory
跳转
- 编程式(js跳转)
this.$router.push('/') - 声明式(标签跳转)
<router-link to=""></router-link>
vue路由传参数
- 使用
query方法传入的参数使用this.$route.query接受 - 使用
params方式传入的参数使用this.$route.params接受
占位
<router-view></router-view>@程序员poetry: 代码已经复制到剪贴板
# vue-router 动态路由是什么
我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个
User组件,对于所有ID各不相同的用户,都要使用这个组件来渲染。那么,我们可以在vue-router的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果
const User = { template: "<div>User</div>", }; const router = new VueRouter({ routes: [ // 动态路径参数 以冒号开头 { path: "/user/:id", component: User }, ], });@程序员poetry: 代码已经复制到剪贴板
问题: vue-router 组件复用导致路由参数失效怎么办?
解决方法:
- 通过
watch监听路由参数再发请求
watch: { //通过watch来监听路由变化 "$route": function(){ this.getData(this.$route.params.xxx); } }@程序员poetry: 代码已经复制到剪贴板
- 用
:key来阻止“复用”
<router-view :key="$route.fullPath" />@程序员poetry: 代码已经复制到剪贴板
回答范例
- 很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由
- 例如,我们可能有一个
User组件,它应该对所有用户进行渲染,但用户ID不同。在Vue Router中,我们可以在路径中使用一个动态字段来实现,例如:{ path: '/users/:id', component: User },其中:id就是路径参数 - 路径参数 用冒号
:表示。当一个路由被匹配时,它的params的值将在每个组件中以this.$route.params的形式暴露出来。 - 参数还可以有多个,例如/
users/:username/posts/:postId;除了$route.params之外,$route对象还公开了其他有用的信息,如$route.query、$route.hash等
::
# router-link和router-view是如何起作用的
分析
vue-router中两个重要组件router-link和router-view,分别起到导航作用和内容渲染作用,但是回答如何生效还真有一定难度
回答范例
vue-router中两个重要组件router-link和router-view,分别起到路由导航作用和组件内容渲染作用- 使用中
router-link默认生成一个a标签,设置to属性定义跳转path。实际上也可以通过custom和插槽自定义最终的展现形式。router-view是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系,配合name可以显示具名组件,起到更强的布局作用。 router-link组件内部根据custom属性判断如何渲染最终生成节点,内部提供导航方法navigate,用户点击之后实际调用的是该方法,此方法最终会修改响应式的路由变量,然后重新去routes匹配出数组结果,router-view则根据其所处深度deep在匹配数组结果中找到对应的路由并获取组件,最终将其渲染出来。
# Vue-router 除了 router-link 怎么实现跳转
声明式导航
<router-link to="/about">Go to About</router-link>@程序员poetry: 代码已经复制到剪贴板
编程式导航
// literal string path router.push('/users/1') // object with path router.push({ path: '/users/1' }) // named route with params to let the router build the url router.push({ name: 'user', params: { username: 'test' } })@程序员poetry: 代码已经复制到剪贴板
回答范例
vue-router导航有两种方式:声明式导航和编程方式导航- 声明式导航方式使用
router-link组件,添加to属性导航;编程方式导航更加灵活,可传递调用router.push(),并传递path字符串或者RouteLocationRaw对象,指定path、name、params等信息 - 如果页面中简单表示跳转链接,使用
router-link最快捷,会渲染一个a标签;如果页面是个复杂的内容,比如商品信息,可以添加点击事件,使用编程式导航 - 实际上内部两者调用的导航函数是一样的
# Vue-router 路由模式有几种
vue-router 有 3 种路由模式:hash、history、abstract,对应的源码如下所示
switch (mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } }@程序员poetry: 代码已经复制到剪贴板
其中,3 种路由模式的说明如下:
hash: 使用URL hash值来作路由,支持所有浏览器history: 依赖HTML5 History API和服务器配置abstract: 支持所有JavaScript运行环境,如Node.js服务器端。如果发现没有浏览器的API,路由会自动强制进入这个模式.
# Vue路由hash模式和history模式
1. hash模式
早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL中 # 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search'
https://interview2.poetries.top#search@程序员poetry: 代码已经复制到剪贴板
hash 路由模式的实现主要是基于下面几个特性
URL中hash值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash部分不会被发送;hash值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash的切换;- 可以通过
a标签,并设置href属性,当用户点击这个标签后,URL的hash值会发生改变;或者使用JavaScript来对loaction.hash进行赋值,改变URL的hash值; - 我们可以使用
hashchange事件来监听hash值的变化,从而对页面进行跳转(渲染)
window.addEventListener("hashchange", funcRef, false);@程序员poetry: 代码已经复制到剪贴板
每一次改变 hash(window.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了
特点:兼容性好但是不美观
2. history模式
history采用HTML5的新特性;且提供了两个新方法: pushState(), replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更
window.history.pushState(null, null, path); window.history.replaceState(null, null, path);@程序员poetry: 代码已经复制到剪贴板
这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。
history 路由模式的实现主要基于存在下面几个特性:
pushState和repalceState两个API来操作实现URL的变化 ;- 我们可以使用
popstate事件来监听url的变化,从而对页面进行跳转(渲染); history.pushState()或history.replaceState()不会触发popstate事件,这时我们需要手动触发页面跳转(渲染)。
特点:虽然美观,但是刷新会出现 404 需要后端进行配置
# 了解history有哪些方法吗?说下它们的区别
history这个对象在html5的时候新加入两个apihistory.pushState()和history.repalceState()这两个API可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。
从参数上来说:
window.history.pushState(state,title,url) //state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取 //title:标题,基本没用,一般传null //url:设定新的历史纪录的url。新的url与当前url的origin必须是一样的,否则会抛出错误。url可以时绝对路径,也可以是相对路径。 //如 当前url是 https://www.baidu.com/a/,执行history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/, //执行history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/ window.history.replaceState(state,title,url) //与pushState 基本相同,但她是修改当前历史纪录,而 pushState 是创建新的历史纪录@程序员poetry: 代码已经复制到剪贴板
另外还有:
window.history.back()后退window.history.forward()前进window.history.go(1)前进或者后退几步
从触发事件的监听上来说:
pushState()和replaceState()不能被popstate事件所监听- 而后面三者可以,且用户点击浏览器前进后退键时也可以
# 如何监听 pushState 和 replaceState 的变化呢?
利用自定义事件new Event()创建这两个事件,并全局监听:
<body> <button onclick="goPage2()">去page2</button> <div>Page1</div> <script> let count = 0; function goPage2 () { history.pushState({ count: count++ }, `bb${count}`,'page1.html') console.log(history) } // 这个不能监听到 pushState // window.addEventListener('popstate', function (event) { // console.log(event) // }) function createHistoryEvent (type) { var fn = history[type] return function () { // 这里的 arguments 就是调用 pushState 时的三个参数集合 var res = fn.apply(this, arguments) let e = new Event(type) e.arguments = arguments window.dispatchEvent(e) return res } } history.pushState = createHistoryEvent('pushState') history.replaceState = createHistoryEvent('replaceState') window.addEventListener('pushState', function (event) { // { type: 'pushState', arguments: [...], target: Window, ... } console.log(event) }) window.addEventListener('replaceState', function (event) { console.log(event) }) </script> </body>@程序员poetry: 代码已经复制到剪贴板
# Vue路由的钩子函数
首页可以控制导航跳转,
beforeEach,afterEach等,一般用于页面title的修改。一些需要登录才能调整页面的重定向功能。
beforeEach主要有3个参数to,from,next。to:route即将进入的目标路由对象。from:route当前导航正要离开的路由。next:function一定要调用该方法resolve这个钩子。执行效果依赖next方法的调用参数。可以控制网页的跳转
# $route和$router的区别
$route是“路由信息对象”,包括path,params,hash,query,fullPath,matched,name等路由信息参数。- 而
$router是“路由实例”对象包括了路由的跳转方法,钩子函数等
# vue-router 路由钩子函数是什么 执行顺序是什么
路由钩子的执行流程, 钩子函数种类有:
全局守卫、路由守卫、组件守卫
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave守卫。 - 调用全局的
beforeEach守卫。 - 在重用的组件里调用
beforeRouteUpdate守卫 (2.2+)。 - 在路由配置里调用
beforeEnter。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter。 - 调用全局的
beforeResolve守卫 (2.5+)。 - 导航被确认。
- 调用全局的
afterEach钩子。 - 触发
DOM更新。 - 调用
beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入
# vue-router 有哪几种导航守卫
- 全局守卫
- 路由独享守卫
- 路由组件内的守卫
全局守卫
vue-router全局有三个守卫
router.beforeEach全局前置守卫 进入路由之前router.beforeResolve全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用router.afterEach全局后置钩子 进入路由之后
// main.js 入口文件 import router from './router'; // 引入路由 router.beforeEach((to, from, next) => { next(); }); router.beforeResolve((to, from, next) => { next(); }); router.afterEach((to, from) => { console.log('afterEach 全局后置钩子'); });@程序员poetry: 代码已经复制到剪贴板
路由独享守卫
如果你不想全局配置守卫的话,你可以为某些路由单独配置守卫
const router = new VueRouter({ routes: [ { path: '/foo', component: Foo, beforeEnter: (to, from, next) => { // 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖 // ... } } ] })@程序员poetry: 代码已经复制到剪贴板
路由组件内的守卫
beforeRouteEnter进入路由前, 在路由独享守卫后调用 不能 获取组件实例this,组件实例还没被创建beforeRouteUpdate(2.2) 路由复用同一个组件时, 在当前路由改变,但是该组件被复用时调用 可以访问组件实例thisbeforeRouteLeave离开当前路由时, 导航离开该组件的对应路由时调用,可以访问组件实例this
# vue-router守卫
导航守卫
router.beforeEach全局前置守卫
to: Route: 即将要进入的目标(路由对象)from: Route: 当前导航正要离开的路由next: Function: 一定要调用该方法来resolve这个钩子。(一定要用这个函数才能去到下一个路由,如果不用就拦截)- 执行效果依赖 next 方法的调用参数。
next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。next(false):取消进入路由,url地址重置为from路由地址(也就是将要离开的路由地址)
// main.js 入口文件 import router from './router'; // 引入路由 router.beforeEach((to, from, next) => { next(); }); router.beforeResolve((to, from, next) => { next(); }); router.afterEach((to, from) => { console.log('afterEach 全局后置钩子'); });@程序员poetry: 代码已经复制到剪贴板
路由独享的守卫 你可以在路由配置上直接定义
beforeEnter守卫
const router = new VueRouter({ routes: [ { path: '/foo', component: Foo, beforeEnter: (to, from, next) => { // ... } } ] })@程序员poetry: 代码已经复制到剪贴板
组件内的守卫你可以在路由组件内直接定义以下路由导航守卫
const Foo = { template: `...`, beforeRouteEnter (to, from, next) { // 在渲染该组件的对应路由被 confirm 前调用 // 不!能!获取组件实例 `this` // 因为当守卫执行前,组件实例还没被创建 }, beforeRouteUpdate (to, from, next) { // 在当前路由改变,但是该组件被复用时调用 // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候, // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。 // 可以访问组件实例 `this` }, beforeRouteLeave (to, from, next) { // 导航离开该组件的对应路由时调用,我们用它来禁止用户离开 // 可以访问组件实例 `this` // 比如还未保存草稿,或者在用户离开前, 将setInterval销毁,防止离开之后,定时器还在调用。 } }@程序员poetry: 代码已经复制到剪贴板
# vue-router中如何保护路由
分析
路由保护在应用开发过程中非常重要,几乎每个应用都要做各种路由权限管理,因此相当考察使用者基本功。
体验
全局守卫:
const router = createRouter({ ... }) router.beforeEach((to, from) => { // ... // 返回 false 以取消导航 return false })@程序员poetry: 代码已经复制到剪贴板
路由独享守卫:
const routes = [ { path: '/users/:id', component: UserDetails, beforeEnter: (to, from) => { // reject the navigation return false }, }, ]@程序员poetry: 代码已经复制到剪贴板
组件内的守卫:
const UserDetails = { template: `...`, beforeRouteEnter(to, from) { // 在渲染该组件的对应路由被验证前调用 }, beforeRouteUpdate(to, from) { // 在当前路由改变,但是该组件被复用时调用 }, beforeRouteLeave(to, from) { // 在导航离开渲染该组件的对应路由时调用 }, }@程序员poetry: 代码已经复制到剪贴板
回答
vue-router中保护路由的方法叫做路由守卫,主要用来通过跳转或取消的方式守卫导航。- 路由守卫有三个级别:
全局、路由独享、组件级。影响范围由大到小,例如全局的router.beforeEach(),可以注册一个全局前置守卫,每次路由导航都会经过这个守卫,因此在其内部可以加入控制逻辑决定用户是否可以导航到目标路由;在路由注册的时候可以加入单路由独享的守卫,例如beforeEnter,守卫只在进入路由时触发,因此只会影响这个路由,控制更精确;我们还可以为路由组件添加守卫配置,例如beforeRouteEnter,会在渲染该组件的对应路由被验证前调用,控制的范围更精确了。 - 用户的任何导航行为都会走
navigate方法,内部有个guards队列按顺序执行用户注册的守卫钩子函数,如果没有通过验证逻辑则会取消原有的导航。
原理
runGuardQueue(guards)链式的执行用户在各级别注册的守卫钩子函数,通过则继续下一个级别的守卫,不通过进入catch流程取消原本导航
// 源码 runGuardQueue(guards) .then(() => { // check global guards beforeEach guards = [] for (const guard of beforeGuards.list()) { guards.push(guardToPromiseFn(guard, to, from)) } guards.push(canceledNavigationCheck) return runGuardQueue(guards) }) .then(() => { // check in components beforeRouteUpdate guards = extractComponentsGuards( updatingRecords, 'beforeRouteUpdate', to, from ) for (const record of updatingRecords) { record.updateGuards.forEach(guard => { guards.push(guardToPromiseFn(guard, to, from)) }) } guards.push(canceledNavigationCheck) // run the queue of per route beforeEnter guards return runGuardQueue(guards) }) .then(() => { // check the route beforeEnter guards = [] for (const record of to.matched) { // do not trigger beforeEnter on reused views if (record.beforeEnter && !from.matched.includes(record)) { if (isArray(record.beforeEnter)) { for (const beforeEnter of record.beforeEnter) guards.push(guardToPromiseFn(beforeEnter, to, from)) } else { guards.push(guardToPromiseFn(record.beforeEnter, to, from)) } } } guards.push(canceledNavigationCheck) // run the queue of per route beforeEnter guards return runGuardQueue(guards) }) .then(() => { // NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component> // clear existing enterCallbacks, these are added by extractComponentsGuards to.matched.forEach(record => (record.enterCallbacks = {})) // check in-component beforeRouteEnter guards = extractComponentsGuards( enteringRecords, 'beforeRouteEnter', to, from ) guards.push(canceledNavigationCheck) // run the queue of per route beforeEnter guards return runGuardQueue(guards) }) .then(() => { // check global guards beforeResolve guards = [] for (const guard of beforeResolveGuards.list()) { guards.push(guardToPromiseFn(guard, to, from)) } guards.push(canceledNavigationCheck) return runGuardQueue(guards) }) // catch any navigation canceled .catch(err => isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED) ? err : Promise.reject(err) )@程序员poetry: 代码已经复制到剪贴板
# 怎么实现路由懒加载呢
这是一道应用题。当打包应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问时才加载对应组件,这样就会更加高效
// 将 // import UserDetails from './views/UserDetails' // 替换为 const UserDetails = () => import('./views/UserDetails') const router = createRouter({ // ... routes: [{ path: '/users/:id', component: UserDetails }], })@程序员poetry: 代码已经复制到剪贴板
回答范例
- 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段
- 一般来说,对所有的路由都使用动态导入是个好主意
- 给
component选项配置一个返回Promise组件的函数就可以定义懒加载路由。例如:{ path: '/users/:id', component: () => import('./views/UserDetails') } - 结合注释
() => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')可以做webpack代码分块
# Vue要做权限管理该怎么做?控制到按钮级别的权限怎么做?
分析
- 综合实践题目,实际开发中经常需要面临权限管理的需求,考查实际应用能力。
- 权限管理一般需求是两个:页面权限和按钮权限,从这两个方面论述即可。

思路
- 权限管理需求分析:页面和按钮权限
- 权限管理的实现方案:分后端方案和前端方案阐述
- 说说各自的优缺点
回答范例
- 权限管理一般需求是页面权限和按钮权限的管理
- 具体实现的时候分后端和前端两种方案:
- 前端方案 会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个
asyncRoutes数组,需要认证的页面在其路由的meta中添加一个roles字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)方式动态添加路由即可 - 后端方案 会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过
addRoutes动态添加路由信息 - 按钮权限的控制通常会
实现一个指令,例如v-permission,将按钮要求角色通过值传给v-permission指令,在指令的moutned钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮
- 纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!
可能的追问
- 类似
Tabs这类组件能不能使用v-permission指令实现按钮权限控制?
<el-tabs> <el-tab-pane label="⽤户管理" name="first">⽤户管理</el-tab-pane> <el-tab-pane label="⻆⾊管理" name="third">⻆⾊管理</el-tab-pane> </el-tabs>@程序员poetry: 代码已经复制到剪贴板
- 服务端返回的路由信息如何添加到路由器中?
// 前端组件名和组件映射表 const map = { //xx: require('@/views/xx.vue').default // 同步的⽅式 xx: () => import('@/views/xx.vue') // 异步的⽅式 } // 服务端返回的asyncRoutes const asyncRoutes = [ { path: '/xx', component: 'xx',... } ] // 遍历asyncRoutes,将component替换为map[component] function mapComponent(asyncRoutes) { asyncRoutes.forEach(route => { route.component = map[route.component]; if(route.children) { route.children.map(child => mapComponent(child)) } }) } mapComponent(asyncRoutes)@程序员poetry: 代码已经复制到剪贴板
# 如果让你从零开始写一个vue路由,说说你的思路
思路分析:
首先思考vue路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。
- 借助
hash或者history api实现url跳转页面不刷新 - 同时监听
hashchange事件或者popstate事件处理跳转 - 根据
hash值或者state值从routes表中匹配对应component并渲染
回答范例:
一个SPA应用的路由需要解决的问题是页面跳转内容改变同时不刷新,同时路由还需要以插件形式存在,所以:
- 首先我会定义一个
createRouter函数,返回路由器实例,实例内部做几件事
- 保存用户传入的配置项
- 监听
hash或者popstate事件 - 回调里根据
path匹配对应路由
- 将
router定义成一个Vue插件,即实现install方法,内部做两件事
- 实现两个全局组件:
router-link和router-view,分别实现页面跳转和内容显示 - 定义两个全局变量:
$route和$router,组件内可以访问当前路由和路由器实例
# 27 Vuex相关
# vuex是什么?怎么使用?哪种功能场景使用它?
Vuex是一个专为Vue.js应用程序开发的状态管理模式。vuex就是一个仓库,仓库里放了很多对象。其中state就是数据源存放地,对应于一般 vue 对象里面的data里面存放的数据是响应式的,vue组件从store读取数据,若是store中的数据发生改变,依赖这相数据的组件也会发生更新它通过mapState把全局的state和getters映射到当前组件的computed计算属性
vuex一般用于中大型web单页应用中对应用的状态进行管理,对于一些组件间关系较为简单的小型应用,使用vuex的必要性不是很大,因为完全可以用组件prop属性或者事件来完成父子组件之间的通信,vuex更多地用于解决跨组件通信以及作为数据中心集中式存储数据。- 使用
Vuex解决非父子组件之间通信问题vuex是通过将state作为数据中心、各个组件共享state实现跨组件通信的,此时的数据完全独立于组件,因此将组件间共享的数据置于State中能有效解决多层级组件嵌套的跨组件通信问题
vuex的State在单页应用的开发中本身具有一个“数据库”的作用,可以将组件中用到的数据存储在State中,并在Action中封装数据读写的逻辑。这时候存在一个问题,一般什么样的数据会放在State中呢? 目前主要有两种数据会使用vuex进行管理:
- 组件之间全局共享的数据
- 通过后端异步请求的数据

包括以下几个模块
state:Vuex使用单一状态树,即每个应用将仅仅包含一个store实例。里面存放的数据是响应式的,vue组件从store读取数据,若是store中的数据发生改变,依赖这相数据的组件也会发生更新。它通过mapState把全局的state和getters映射到当前组件的computed计算属性mutations:更改Vuex的store中的状态的唯一方法是提交mutationgetters:getter可以对state进行计算操作,它就是store的计算属性虽然在组件内也可以做计算属性,但是getters可以在多给件之间复用如果一个状态只在一个组件内使用,是可以不用gettersaction:action类似于muation, 不同在于:action提交的是mutation,而不是直接变更状态action可以包含任意异步操作modules:面对复杂的应用程序,当管理的状态比较多时;我们需要将vuex的store对象分割成模块(modules)

modules:项目特别复杂的时候,可以让每一个模块拥有自己的state、mutation、action、getters,使得结构非常清晰,方便管理

回答范例
思路
- 给定义
- 必要性阐述
- 何时使用
- 拓展:一些个人思考、实践经验等
回答范例
Vuex是一个专为Vue.js应用开发的状态管理模式 + 库。它采用集中式存储,管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。- 我们期待以一种简单的“单向数据流”的方式管理应用,即状态 -> 视图 -> 操作单向循环的方式。但当我们的应用遇到多个组件共享状态时,比如:多个视图依赖于同一状态或者来自不同视图的行为需要变更同一状态。此时单向数据流的简洁性很容易被破坏。因此,我们有必要把组件的共享状态抽取出来,以一个全局单例模式管理。通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这是
vuex存在的必要性,它和react生态中的redux之类是一个概念 Vuex解决状态管理的同时引入了不少概念:例如state、mutation、action等,是否需要引入还需要根据应用的实际情况衡量一下:如果不打算开发大型单页应用,使用Vuex反而是繁琐冗余的,一个简单的store模式就足够了。但是,如果要构建一个中大型单页应用,Vuex基本是标配。- 我在使用
vuex过程中感受到一些等
可能的追问
vuex有什么缺点吗?你在开发过程中有遇到什么问题吗?
- 刷新浏览器,
vuex中的state会重新变为初始状态。解决方案-插件vuex-persistedstate
action和mutation的区别是什么?为什么要区分它们?
action中处理异步,mutation不可以mutation做原子操作action可以整合多个mutation的集合mutation是同步更新数据(内部会进行是否为异步方式更新数据的检测)$watch严格模式下会报错action异步操作,可以获取数据后调佣mutation提交最终数据
- 流程顺序:“相应视图—>修改State”拆分成两部分,视图触发
Action,Action再触发Mutation`。 - 基于流程顺序,二者扮演不同的角色:
Mutation:专注于修改State,理论上是修改State的唯一途径。Action:业务代码、异步请求 - 角色不同,二者有不同的限制:
Mutation:必须同步执行。Action:可以异步,但不能直接操作State
::
# Vuex中actions和mutations有什么区别
题目分析
mutations和actions是vuex带来的两个独特的概念。新手程序员容易混淆,所以面试官喜欢问。- 我们只需记住修改状态只能是
mutations,actions只能通过提交mutation修改状态即可
回答范例
- 更改
Vuex的store中的状态的唯一方法是提交mutation,mutation非常类似于事件:每个mutation都有一个字符串的类型 (type)和一个 回调函数 (handler) 。Action类似于mutation,不同在于:Action可以包含任意异步操作,但它不能修改状态, 需要提交mutation才能变更状态 - 开发时,包含异步操作或者复杂业务组合时使用
action;需要直接修改状态则提交mutation。但由于dispatch和commit是两个API,容易引起混淆,实践中也会采用统一使用dispatch action的方式。调用dispatch和commit两个API时几乎完全一样,但是定义两者时却不甚相同,mutation的回调函数接收参数是state对象。action则是与Store实例具有相同方法和属性的上下文context对象,因此一般会解构它为{commit, dispatch, state},从而方便编码。另外dispatch会返回Promise实例便于处理内部异步结果 - 实现上
commit(type)方法相当于调用options.mutations[type](state);dispatch(type)方法相当于调用options.actions[type](store),这样就很容易理解两者使用上的不同了
实现
我们可以像下面这样简单实现commit和dispatch,从而辨别两者不同
class Store { constructor(options) { this.state = reactive(options.state) this.options = options } commit(type, payload) { // 传入上下文和参数1都是state对象 this.options.mutations[type].call(this.state, this.state, payload) } dispatch(type, payload) { // 传入上下文和参数1都是store本身 this.options.actions[type].call(this, this, payload) } }@程序员poetry: 代码已经复制到剪贴板
# 怎么监听vuex数据的变化
分析
vuex数据状态是响应式的,所以状态变视图跟着变,但是有时还是需要知道数据状态变了从而做一些事情。- 既然状态都是响应式的,那自然可以
watch,另外vuex也提供了订阅的API:store.subscribe()
回答范例
- 我知道几种方法:
- 可以通过
watch选项或者watch方法监听状态 - 可以使用
vuex提供的API:store.subscribe()
watch选项方式,可以以字符串形式监听$store.state.xx;subscribe方式,可以调用store.subscribe(cb),回调函数接收mutation对象和state对象,这样可以进一步判断mutation.type是否是期待的那个,从而进一步做后续处理。watch方式简单好用,且能获取变化前后值,首选;subscribe方法会被所有commit行为触发,因此还需要判断mutation.type,用起来略繁琐,一般用于vuex插件中
实践
watch方式
const app = createApp({ watch: { '$store.state.counter'() { console.log('counter change!'); } } })@程序员poetry: 代码已经复制到剪贴板
subscribe方式:
store.subscribe((mutation, state) => { if (mutation.type === 'add') { console.log('counter change in subscribe()!'); } })@程序员poetry: 代码已经复制到剪贴板
# Vuex 页面刷新数据丢失怎么解决
体验
可以从localStorage中获取作为状态初始值:
const store = createStore({ state () { return { count: localStorage.getItem('count') } } })@程序员poetry: 代码已经复制到剪贴板
业务代码中,提交修改状态同时保存最新值:虽说实现了,但是每次还要手动刷新localStorage不太优雅
store.commit('increment') localStorage.setItem('count', store.state.count)@程序员poetry: 代码已经复制到剪贴板
回答范例
vuex只是在内存保存状态,刷新之后就会丢失,如果要持久化就要存起来localStorage就很合适,提交mutation的时候同时存入localStorage,store中把值取出作为state的初始值即可。- 这里有两个问题,不是所有状态都需要持久化;如果需要保存的状态很多,编写的代码就不够优雅,每个提交的地方都要单独做保存处理。这里就可以利用
vuex提供的subscribe方法做一个统一的处理。甚至可以封装一个vuex插件以便复用。 - 类似的插件有
vuex-persist、vuex-persistedstate,内部的实现就是通过订阅mutation变化做统一处理,通过插件的选项控制哪些需要持久化
原理
可以看一下vuex-persist (opens new window)内部确实是利用subscribe实现的
# Vuex 为什么要分模块并且加命名空间
- 模块: 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,
store对象就有可能变得相当臃肿。为了解决以上问题,Vuex允许我们将store分割成模块(module)。每个模块拥有自己的state、mutation、action、getter、甚至是嵌套子模块 - 命名空间:默认情况下,模块内部的
action、mutation和getter是注册在全局命名空间的——这样使得多个模块能够对同一mutation或action作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加namespaced: true的方式使其成为带命名空间的模块。当模块被注册后,它的所有getter、action及mutation都会自动根据模块注册的路径调整命名
# 你有使用过vuex的module吗?
const moduleA = { state: () => ({ ... }), mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { state: () => ({ ... }), mutations: { ... }, actions: { ... } } const store = createStore({ modules: { a: moduleA, b: moduleB } }) store.state.a // -> moduleA 的状态 store.state.b // -> moduleB 的状态 store.getters.c // -> moduleA里的getters store.commit('d') // -> 能同时触发子模块中同名mutation store.dispatch('e') // -> 能同时触发子模块中同名action@程序员poetry: 代码已经复制到剪贴板
- 用过
module,项目规模变大之后,单独一个store对象会过于庞大臃肿,通过模块方式可以拆分开来便于维护 - 可以按之前规则单独编写子模块代码,然后在主文件中通过
modules选项组织起来:reateStore({modules:{...}}) - 不过使用时要注意访问子模块状态时需要加上注册时模块名:
store.state.a.xxx,但同时getters、mutations和actions又在全局空间中,使用方式和之前一样。如果要做到完全拆分,需要在子块加上namespace选项,此时再访问它们就要加上命名空间前缀。 - 很显然,模块的方式可以拆分代码,但是缺点也很明显,就是使用起来比较繁琐复杂,容易出错。而且类型系统支持很差,不能给我们带来帮助。
pinia显然在这方面有了很大改进,是时候切换过去了
# 你觉得vuex有什么缺点
分析
相较于redux,vuex已经相当简便好用了。但模块的使用比较繁琐,对ts支持也不好。
体验
使用模块:用起来比较繁琐,使用模式也不统一,基本上得不到类型系统的任何支持
const store = createStore({ modules: { a: moduleA } }) store.state.a // -> 要带上 moduleA 的key,内嵌模块的话会很长,不得不配合mapState使用 store.getters.c // -> moduleA里的getters,没有namespaced时又变成了全局的 store.getters['a/c'] // -> 有namespaced时要加path,使用模式又和state不一样 store.commit('d') // -> 没有namespaced时变成了全局的,能同时触发多个子模块中同名mutation store.commit('a/d') // -> 有namespaced时要加path,配合mapMutations使用感觉也没简化@程序员poetry: 代码已经复制到剪贴板
回答范例
vuex利用响应式,使用起来已经相当方便快捷了。但是在使用过程中感觉模块化这一块做的过于复杂,用的时候容易出错,还要经常查看文档- 比如:访问
state时要带上模块key,内嵌模块的话会很长,不得不配合mapState使用,加不加namespaced区别也很大,getters,mutations,actions这些默认是全局,加上之后必须用字符串类型的path来匹配,使用模式不统一,容易出错;对ts的支持也不友好,在使用模块时没有代码提示。 - 之前
Vue2项目中用过vuex-module-decorators的解决方案,虽然类型支持上有所改善,但又要学一套新东西,增加了学习成本。pinia出现之后使用体验好了很多,Vue3 + pinia会是更好的组合
原理
下面我们来看看vuex中store.state.x.y这种嵌套的路径是怎么搞出来的
首先是子模块安装过程:父模块状态
parentState上面设置了子模块名称moduleName,值为当前模块state对象。放在上面的例子中相当于:store.state['x'] = moduleX.state。此过程是递归的,那么store.state.x.y安装时就是:store.state['x']['y'] = moduleY.state
//源码位置 https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115 if (!isRoot && !hot) { // 获取父模块state const parentState = getNestedState(rootState, path.slice(0, -1)) // 获取子模块名称 const moduleName = path[path.length - 1] store._withCommit(() => { // 把子模块state设置到父模块上 parentState[moduleName] = module.state }) }@程序员poetry: 代码已经复制到剪贴板
# 用过pinia吗?有什么优点?
1. pinia是什么?
- 在
Vue3中,可以使用传统的Vuex来实现状态管理,也可以使用最新的pinia来实现状态管理,我们来看看官网如何解释pinia的:Pinia是Vue的存储库,它允许您跨组件/页面共享状态。- 实际上,
pinia就是Vuex的升级版,官网也说过,为了尊重原作者,所以取名pinia,而没有取名Vuex,所以大家可以直接将pinia比作为Vue3的Vuex
2. 为什么要使用pinia?
Vue2和Vue3都支持,这让我们同时使用Vue2和Vue3的小伙伴都能很快上手。pinia中只有state、getter、action,抛弃了Vuex中的Mutation,Vuex中mutation一直都不太受小伙伴们的待见,pinia直接抛弃它了,这无疑减少了我们工作量。pinia中action支持同步和异步,Vuex不支持- 良好的
Typescript支持,毕竟我们Vue3都推荐使用TS来编写,这个时候使用pinia就非常合适了 - 无需再创建各个模块嵌套了,
Vuex中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia中每个store都是独立的,互相不影响。 - 体积非常小,只有
1KB左右。 pinia支持插件来扩展自身功能。- 支持服务端渲染
3. pinna使用
- 准备工作
我们这里搭建一个最新的Vue3 + TS + Vite项目
npm create vite@latest my-vite-app --template vue-ts@程序员poetry: 代码已经复制到剪贴板
pinia基础使用
yarn add pinia@程序员poetry: 代码已经复制到剪贴板
// main.ts import { createApp } from "vue"; import App from "./App.vue"; import { createPinia } from "pinia"; const pinia = createPinia(); const app = createApp(App); app.use(pinia); app.mount("#app");@程序员poetry: 代码已经复制到剪贴板
2.1 创建store
//sbinsrc/store/user.ts import { defineStore } from 'pinia' // 第一个参数是应用程序中 store 的唯一 id export const useUsersStore = defineStore('users', { // 其它配置项 })@程序员poetry: 代码已经复制到剪贴板
创建store很简单,调用pinia中的defineStore函数即可,该函数接收两个参数:
name:一个字符串,必传项,该store的唯一id。options:一个对象,store的配置项,比如配置store内的数据,修改数据的方法等等。
我们可以定义任意数量的store,因为我们其实一个store就是一个函数,这也是pinia的好处之一,让我们的代码扁平化了,这和Vue3的实现思想是一样的
2.2 使用store
<!-- src/App.vue --> <script setup lang="ts"> import { useUsersStore } from "../src/store/user"; const store = useUsersStore(); console.log(store); </script>@程序员poetry: 代码已经复制到剪贴板
2.3 添加state
export const useUsersStore = defineStore("users", { state: () => { return { name: "test", age: 20, sex: "男", }; }, });@程序员poetry: 代码已经复制到剪贴板
2.4 读取state数据
<template> <img alt="Vue logo" src="./assets/logo.png" /> <p>姓名:{{ name }}</p> <p>年龄:{{ age }}</p> <p>性别:{{ sex }}</p> </template> <script setup lang="ts"> import { ref } from "vue"; import { useUsersStore } from "../src/store/user"; const store = useUsersStore(); const name = ref<string>(store.name); const age = ref<number>(store.age); const sex = ref<string>(store.sex); </script>@程序员poetry: 代码已经复制到剪贴板
上段代码中我们直接通过store.age等方式获取到了store存储的值,但是大家有没有发现,这样比较繁琐,我们其实可以用解构的方式来获取值,使得代码更简洁一点
import { useUsersStore, storeToRefs } from "../src/store/user"; const store = useUsersStore(); const { name, age, sex } = storeToRefs(store); // storeToRefs获取的值是响应式的@程序员poetry: 代码已经复制到剪贴板
2.5 修改state数据
<template> <img alt="Vue logo" src="./assets/logo.png" /> <p>姓名:{{ name }}</p> <p>年龄:{{ age }}</p> <p>性别:{{ sex }}</p> <button @click="changeName">更改姓名</button> </template> <script setup lang="ts"> import child from './child.vue'; import { useUsersStore, storeToRefs } from "../src/store/user"; const store = useUsersStore(); const { name, age, sex } = storeToRefs(store); const changeName = () => { store.name = "张三"; console.log(store); }; </script>@程序员poetry: 代码已经复制到剪贴板
2.6 重置state
- 有时候我们修改了
state数据,想要将它还原,这个时候该怎么做呢?就比如用户填写了一部分表单,突然想重置为最初始的状态。 - 此时,我们直接调用
store的$reset()方法即可,继续使用我们的例子,添加一个重置按钮
<button @click="reset">重置store</button> // 重置store const reset = () => { store.$reset(); };@程序员poetry: 代码已经复制到剪贴板
当我们点击重置按钮时,store中的数据会变为初始状态,页面也会更新
2.7 批量更改state数据
如果我们一次性需要修改很多条数据的话,有更加简便的方法,使用store的$patch方法,修改app.vue代码,添加一个批量更改数据的方法
<button @click="patchStore">批量修改数据</button> // 批量修改数据 const patchStore = () => { store.$patch({ name: "张三", age: 100, sex: "女", }); };@程序员poetry: 代码已经复制到剪贴板
- 有经验的小伙伴可能发现了,我们采用这种批量更改的方式似乎代价有一点大,假如我们
state中有些字段无需更改,但是按照上段代码的写法,我们必须要将state中的所有字段例举出了。 - 为了解决该问题,
pinia提供的$patch方法还可以接收一个回调函数,它的用法有点像我们的数组循环回调函数了。
store.$patch((state) => { state.items.push({ name: 'shoes', quantity: 1 }) state.hasChanged = true })@程序员poetry: 代码已经复制到剪贴板
2.8 直接替换整个state
pinia提供了方法让我们直接替换整个state对象,使用store的$state方法
store.$state = { counter: 666, name: '张三' }@程序员poetry: 代码已经复制到剪贴板
上段代码会将我们提前声明的state替换为新的对象,可能这种场景用得比较少
getters属性
getters是defineStore参数配置项里面的另一个属性- 可以把
getter想象成Vue中的计算属性,它的作用就是返回一个新的结果,既然它和Vue中的计算属性类似,那么它肯定也是会被缓存的,就和computed一样
3.1 添加getter
export const useUsersStore = defineStore("users", { state: () => { return { name: "test", age: 10, sex: "男", }; }, getters: { getAddAge: (state) => { return state.age + 100; }, }, })@程序员poetry: 代码已经复制到剪贴板
上段代码中我们在配置项参数中添加了getter属性,该属性对象中定义了一个getAddAge方法,该方法会默认接收一个state参数,也就是state对象,然后该方法返回的是一个新的数据
3.2 使用getter
<template> <p>新年龄:{{ store.getAddAge }}</p> <button @click="patchStore">批量修改数据</button> </template> <script setup lang="ts"> import { useUsersStore } from "../src/store/user"; const store = useUsersStore(); // 批量修改数据 const patchStore = () => { store.$patch({ name: "张三", age: 100, sex: "女", }); }; </script>@程序员poetry: 代码已经复制到剪贴板
上段代码中我们直接在标签上使用了store.gettAddAge方法,这样可以保证响应式,其实我们state中的name等属性也可以以此种方式直接在标签上使用,也可以保持响应式
3.3 getter中调用其它getter
export const useUsersStore = defineStore("users", { state: () => { return { name: "test", age: 20, sex: "男", }; }, getters: { getAddAge: (state) => { return state.age + 100; }, getNameAndAge(): string { return this.name + this.getAddAge; // 调用其它getter }, }, });@程序员poetry: 代码已经复制到剪贴板
3.3 getter传参
export const useUsersStore = defineStore("users", { state: () => { return { name: "test", age: 20, sex: "男", }; }, getters: { getAddAge: (state) => { return (num: number) => state.age + num; }, getNameAndAge(): string { return this.name + this.getAddAge; // 调用其它getter }, }, });@程序员poetry: 代码已经复制到剪贴板
<p>新年龄:{{ store.getAddAge(1100) }}</p>@程序员poetry: 代码已经复制到剪贴板
actions属性
- 前面我们提到的
state和getters属性都主要是数据层面的,并没有具体的业务逻辑代码,它们两个就和我们组件代码中的data数据和computed计算属性一样。 - 那么,如果我们有业务代码的话,最好就是卸载
actions属性里面,该属性就和我们组件代码中的methods相似,用来放置一些处理业务逻辑的方法。 actions属性值同样是一个对象,该对象里面也是存储的各种各样的方法,包括同步方法和异步方法
4.1 添加actions
export const useUsersStore = defineStore("users", { state: () => { return { name: "test", age: 20, sex: "男", }; }, getters: { getAddAge: (state) => { return (num: number) => state.age + num; }, getNameAndAge(): string { return this.name + this.getAddAge; // 调用其它getter }, }, actions: { // 在实际场景中,该方法可以是任何逻辑,比如发送请求、存储token等等。大家把actions方法当作一个普通的方法即可,特殊之处在于该方法内部的this指向的是当前store saveName(name: string) { this.name = name; }, }, });@程序员poetry: 代码已经复制到剪贴板
4.2 使用actions
使用actions中的方法也非常简单,比如我们在App.vue中想要调用该方法
const saveName = () => { store.saveName("poetries"); };@程序员poetry: 代码已经复制到剪贴板
总结
pinia的知识点很少,如果你有Vuex基础,那么学起来更是易如反掌
pinia无非就是以下3个大点:
stategettersactions
# 28 对Vue SSR的理解
Vue.js是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出Vue组件,进行生成DOM和操作DOM。然而,也可以将同一个组件渲染为服务端的HTML字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端
- 优点:
SSR有着更好的SEO、并且首屏加载速度更快- 因为
SPA页面的内容是通过Ajax获取,而搜索引擎爬取工具并不会等待Ajax异步完成后再抓取页面内容,所以在SPA中是抓取不到页面通过Ajax获取到的内容;而SSR是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面 - 更快的内容到达时间(首屏加载更快):
SPA会等待所有Vue编译后的js文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间
- 因为
- 缺点: 开发条件会受到限制,服务器端渲染只支持
beforeCreate和created两个钩子,当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境。服务器会有更大的负载需求- 在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的
server更加大量占用CPU资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 ( high traffic ) 下使用,请准备相应的服务器负载,并明智地采用缓存策略
- 在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的
其基本实现原理
app.js作为客户端与服务端的公用入口,导出Vue根实例,供客户端entry与服务端entry使用。客户端entry主要作用挂载到DOM上,服务端entry除了创建和返回实例,还进行路由匹配与数据预获取。webpack为客服端打包一个Client Bundle,为服务端打包一个Server Bundle。- 服务器接收请求时,会根据
url,加载相应组件,获取和解析异步数据,创建一个读取Server Bundle的BundleRenderer,然后生成html发送给客户端。 - 客户端混合,客户端收到从服务端传来的
DOM与自己的生成的 DOM 进行对比,把不相同的DOM激活,使其可以能够响应后续变化,这个过程称为客户端激活 。为确保混合成功,客户端与服务器端需要共享同一套数据。在服务端,可以在渲染之前获取数据,填充到stroe里,这样,在客户端挂载到DOM之前,可以直接从store里取数据。首屏的动态数据通过window.__INITIAL_STATE__发送到客户端
Vue SSR的实现,主要就是把Vue的组件输出成一个完整HTML,vue-server-renderer就是干这事的
Vue SSR需要做的事多点(输出完整 HTML),除了complier -> vnode,还需如数据获取填充至 HTML、客户端混合(hydration)、缓存等等。相比于其他模板引擎(ejs, jade 等),最终要实现的目的是一样的,性能上可能要差点
# 29 Vue 修饰符有哪些
事件修饰符
.stop阻止事件继续传播.prevent阻止标签默认行为.capture使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理.self只当在event.target是当前元素自身时触发处理函数.once事件将只会触发一次.passive告诉浏览器你不想阻止事件的默认行为
v-model 的修饰符
.lazy通过这个修饰符,转变为在change事件再同步.number自动将用户的输入值转化为数值类型.trim自动过滤用户输入的首尾空格
键盘事件的修饰符
.enter.tab.delete(捕获“删除”和“退格”键).esc.space.up.down.left.right
系统修饰键
.ctrl.alt.shift.meta
鼠标按钮修饰符
.left.right.middle
# 30 说说 vue 内置指令

# 31 怎样理解 Vue 的单向数据流
数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解
注意:在子组件直接用 v-model 绑定父组件传过来的 prop 这样是不规范的写法 开发环境会报警告
如果实在要改变父组件的 prop 值,可以在 data 里面定义一个变量 并用 prop 的值初始化它 之后用$emit 通知父组件去修改
有两种常见的试图改变一个 prop 的情形 :
- 这个
prop用来传递一个初始值;这个子组件接下来希望将其作为一个本地的prop数据来使用。 在这种情况下,最好定义一个本地的data属性并将这个prop用作其初始值
props: ['initialCounter'], data: function () { return { counter: this.initialCounter } }@程序员poetry: 代码已经复制到剪贴板
- 这个
prop以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个prop的值来定义一个计算属性
props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }@程序员poetry: 代码已经复制到剪贴板
# 32 写过自定义指令吗?原理是什么
回答范例
Vue有一组默认指令,比如v-model或v-for,同时Vue也允许用户注册自定义指令来扩展Vue能力- 自定义指令主要完成一些可复用低层级
DOM操作 - 使用自定义指令分为定义、注册和使用三步:
- 定义自定义指令有两种方式:对象和函数形式,前者类似组件定义,有各种生命周期;后者只会在
mounted和updated时执行 - 注册自定义指令类似组件,可以使用
app.directive()全局注册,使用{directives:{xxx}}局部注册 - 使用时在注册名称前加上
v-即可,比如v-focus
- 我在项目中常用到一些自定义指令,例如:
- 复制粘贴
v-copy - 长按
v-longpress - 防抖
v-debounce - 图片懒加载
v-lazy - 按钮权限
v-premission - 页面水印
v-waterMarker - 拖拽指令
v-draggable
vue3中指令定义发生了比较大的变化,主要是钩子的名称保持和组件一致,这样开发人员容易记忆,不易犯错。另外在v3.2之后,可以在setup中以一个小写v开头方便的定义自定义指令,更简单了
# 基本使用
当Vue中的核心内置指令不能够满足我们的需求时,我们可以定制自定义的指令用来满足开发的需求
我们看到的v-开头的行内属性,都是指令,不同的指令可以完成或实现不同的功能,对普通 DOM元素进行底层操作,这时候就会用到自定义指令。除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令
// 指令使用的几种方式: //会实例化一个指令,但这个指令没有参数 `v-xxx` // -- 将值传到指令中 `v-xxx="value"` // -- 将字符串传入到指令中,如`v-html="'<p>内容</p>'"` `v-xxx="'string'"` // -- 传参数(`arg`),如`v-bind:class="className"` `v-xxx:arg="value"` // -- 使用修饰符(`modifier`) `v-xxx:arg.modifier="value"`@程序员poetry: 代码已经复制到剪贴板
注册一个自定义指令有全局注册与局部注册
// 全局注册注册主要是用过Vue.directive方法进行注册 // Vue.directive第一个参数是指令的名字(不需要写上v-前缀),第二个参数可以是对象数据,也可以是一个指令函数 // 注册一个全局自定义指令 `v-focus` Vue.directive('focus', { // 当被绑定的元素插入到 DOM 中时…… inserted: function (el) { // 聚焦元素 el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能 } }) // 局部注册通过在组件options选项中设置directive属性 directives: { focus: { // 指令的定义 inserted: function (el) { el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能 } } } // 然后你可以在模板中任何元素上使用新的 v-focus property,如下: <input v-focus />@程序员poetry: 代码已经复制到剪贴板
钩子函数
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。componentUpdated:被绑定元素所在模板完成一次更新周期时调用。unbind:只调用一次,指令与元素解绑时调用。
所有的钩子函数的参数都有以下:
el:指令所绑定的元素,可以用来直接操作 DOMbinding:一个对象,包含以下property:name:指令名,不包括v-前缀。value:指令的绑定值,例如:v-my-directive="1 + 1"中,绑定值为2。oldValue:指令绑定的前一个值,仅在update和componentUpdated钩子中可用。无论值是否改变都可用。expression:字符串形式的指令表达式。例如v-my-directive="1 + 1"中,表达式为"1 + 1"。arg:传给指令的参数,可选。例如v-my-directive:foo中,参数为"foo"。modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar中,修饰符对象为{ foo: true, bar: true }vnode:Vue编译生成的虚拟节点oldVnode:上一个虚拟节点,仅在update和componentUpdated钩子中可用
除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行
<div v-demo="{ color: 'white', text: 'hello!' }"></div> <script> Vue.directive('demo', function (el, binding) { console.log(binding.value.color) // "white" console.log(binding.value.text) // "hello!" }) </script>@程序员poetry: 代码已经复制到剪贴板
应用场景
使用自定义组件组件可以满足我们日常一些场景,这里给出几个自定义组件的案例:
- 防抖
// 1.设置v-throttle自定义指令 Vue.directive('throttle', { bind: (el, binding) => { let throttleTime = binding.value; // 防抖时间 if (!throttleTime) { // 用户若不设置防抖时间,则默认2s throttleTime = 2000; } let cbFun; el.addEventListener('click', event => { if (!cbFun) { // 第一次执行 cbFun = setTimeout(() => { cbFun = null; }, throttleTime); } else { event && event.stopImmediatePropagation(); } }, true); }, }); // 2.为button标签设置v-throttle自定义指令 <button @click="sayHello" v-throttle>提交</button>@程序员poetry: 代码已经复制到剪贴板
- 图片懒加载
设置一个v-lazy自定义组件完成图片懒加载
const LazyLoad = { // install方法 install(Vue,options){ // 代替图片的loading图 let defaultSrc = options.default; Vue.directive('lazy',{ bind(el,binding){ LazyLoad.init(el,binding.value,defaultSrc); }, inserted(el){ // 兼容处理 if('InterpObserver' in window){ LazyLoad.observe(el); }else{ LazyLoad.listenerScroll(el); } }, }) }, // 初始化 init(el,val,def){ // src 储存真实src el.setAttribute('src',val); // 设置src为loading图 el.setAttribute('src',def); }, // 利用InterpObserver监听el observe(el){ let io = new InterpObserver(entries => { let realSrc = el.dataset.src; if(entries[0].isIntersecting){ if(realSrc){ el.src = realSrc; el.removeAttribute('src'); } } }); io.observe(el); }, // 监听scroll事件 listenerScroll(el){ let handler = LazyLoad.throttle(LazyLoad.load,300); LazyLoad.load(el); window.addEventListener('scroll',() => { handler(el); }); }, // 加载真实图片 load(el){ let windowHeight = document.documentElement.clientHeight let elTop = el.getBoundingClientRect().top; let elBtm = el.getBoundingClientRect().bottom; let realSrc = el.dataset.src; if(elTop - windowHeight<0&&elBtm > 0){ if(realSrc){ el.src = realSrc; el.removeAttribute('src'); } } }, // 节流 throttle(fn,delay){ let timer; let prevTime; return function(...args){ let currTime = Date.now(); let context = this; if(!prevTime) prevTime = currTime; clearTimeout(timer); if(currTime - prevTime > delay){ prevTime = currTime; fn.apply(context,args); clearTimeout(timer); return; } timer = setTimeout(function(){ prevTime = Date.now(); timer = null; fn.apply(context,args); },delay); } } } export default LazyLoad;@程序员poetry: 代码已经复制到剪贴板
- 一键 Copy的功能
import { Message } from 'ant-design-vue'; const vCopy = { // /* bind 钩子函数,第一次绑定时调用,可以在这里做初始化设置 el: 作用的 dom 对象 value: 传给指令的值,也就是我们要 copy 的值 */ bind(el, { value }) { el.$value = value; // 用一个全局属性来存传进来的值,因为这个值在别的钩子函数里还会用到 el.handler = () => { if (!el.$value) { // 值为空的时候,给出提示,我这里的提示是用的 ant-design-vue 的提示,你们随意 Message.warning('无复制内容'); return; } // 动态创建 textarea 标签 const textarea = document.createElement('textarea'); // 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域 textarea.readOnly = 'readonly'; textarea.style.position = 'absolute'; textarea.style.left = '-9999px'; // 将要 copy 的值赋给 textarea 标签的 value 属性 textarea.value = el.$value; // 将 textarea 插入到 body 中 document.body.appendChild(textarea); // 选中值并复制 textarea.select(); // textarea.setSelectionRange(0, textarea.value.length); const result = document.execCommand('Copy'); if (result) { Message.success('复制成功'); } document.body.removeChild(textarea); }; // 绑定点击事件,就是所谓的一键 copy 啦 el.addEventListener('click', el.handler); }, // 当传进来的值更新的时候触发 componentUpdated(el, { value }) { el.$value = value; }, // 指令与元素解绑的时候,移除事件绑定 unbind(el) { el.removeEventListener('click', el.handler); }, }; export default vCopy;@程序员poetry: 代码已经复制到剪贴板
- 拖拽
<div ref="a" id="bg" v-drag></div> directives: { drag: { bind() {}, inserted(el) { el.onmousedown = (e) => { let x = e.clientX - el.offsetLeft; let y = e.clientY - el.offsetTop; document.onmousemove = (e) => { let xx = e.clientX - x + "px"; let yy = e.clientY - y + "px"; el.style.left = xx; el.style.top = yy; }; el.onmouseup = (e) => { document.onmousemove = null; }; }; }, }, }@程序员poetry: 代码已经复制到剪贴板
# 原理
- 指令本质上是装饰器,是
vue对HTML元素的扩展,给HTML元素增加自定义功能。vue编译DOM时,会找到指令对象,执行指令的相关方法。 - 自定义指令有五个生命周期(也叫钩子函数),分别是
bind、inserted、update、componentUpdated、unbind
原理
- 在生成
ast语法树时,遇到指令会给当前元素添加directives属性 - 通过
genDirectives生成指令代码 - 在
patch前将指令的钩子提取到cbs中,在patch过程中调用对应的钩子 - 当执行指令对应钩子函数时,调用对应指令定义的方法
# vue3.2 自定义全局指令、局部指令
// 在src目录下新建一个directive文件,在此文件夹下新建一个index.js文件夹,接着输入如下内容 const directives = (app) => { //这里是给元素取得名字,虽然是focus,但是实际引用的时候必须以v开头 app.directive('focus',{ //这里的el就是获取的元素 mounted(el) { el.focus() } }) } //默认导出 directives export default directives@程序员poetry: 代码已经复制到剪贴板
// 在全局注册directive import { createApp } from 'vue' import App from './App.vue' import router from './router' import store from './store' import directives from './directives' const app = createApp(App) directives(app) app.use(store).use(router).mount('#app')@程序员poetry: 代码已经复制到剪贴板
<!-- 在你需要的页面进行自定义指令的使用 --> <template> <div class="container"> <div class="content"> <input type="text" v-focus> 内容 </div> </div> </template> <script setup> import { reactive, ref } from 'vue' // const vMove:Directive = () =>{ // } </script>@程序员poetry: 代码已经复制到剪贴板
在
vue3.2 setup语法糖模式下,自定义指令变得及其简单
<input type="text" v-model="value" v-focus> <script setup> //直接写,但是必须是v开头 const vFocus = { mounted(el) { // 获取input,并调用其focus()方法 el.focus() } } </script>@程序员poetry: 代码已经复制到剪贴板
<!-- demo 进去页面自动获取焦点,然后让盒子的颜色根据你input框输入的内容变色,并且作防抖处理 --> <template> <div class="container"> <div class="content" v-move="{ background: value }"> 内容 <input type="text" v-model="value" v-focus @keyup="see"> </div> </div> </template> <script setup> import { reactive, ref } from 'vue' const value = ref('') const vFocus = { mounted(el) { // 获取input,并调用其focus()方法 el.focus() } } let timer = null const vMove = (el, binding) => { if (timer !== null) { clearTimeout(timer) } timer = setTimeout(() => { el.style.background = binding.value.background console.log(el); }, 1000); } </script> <style lang="scss" scoped> .container { width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; .content { border-top: 5px solid black; width: 200px; height: 200px; cursor: pointer; border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-bottom: 1px solid #ccc; } } </style>@程序员poetry: 代码已经复制到剪贴板
# 33 Vue3相关
# 你知道哪些Vue3新特性?
官网列举的最值得注意的新特性:v3-migration.vuejs.org (opens new window)

Composition APISFC Composition API语法糖Teleport传送门Fragments片段Emits选项- 自定义渲染器
SFC CSS变量Suspense
以上这些是api相关,另外还有很多框架特性也不能落掉
回答范例
api层面Vue3新特性主要包括:Composition API、SFC Composition API语法糖、Teleport传送门、Fragments片段、Emits选项、自定义渲染器、SFC CSS变量、Suspense- 另外,
Vue3.0在框架层面也有很多亮眼的改进:
- 更快
- 虚拟
DOM重写,diff算法优化 - 编译器优化:静态提升、
patchFlags(静态标记)、事件监听缓存 - 基于
Proxy的响应式系统 SSR优化
- 虚拟
- 更小:更好的摇树优化
tree shaking、Vue3移除一些不常用的API - 更友好:
vue3在兼顾vue2的options API的同时还推出了composition API,大大增加了代码的逻辑组织和代码复用能力 - 更容易维护:
TypeScript+ 模块化 - 更容易扩展
- 独立的响应化模块
- 自定义渲染器
# Vue3速度快的原因
Vue3.0 性能提升体现在哪些方面
- 代码层面性能优化主要体现在全新响应式
API,基于Proxy实现,初始化时间和内存占用均大幅改进; - 编译层面做了更多编译优化处理,比如
静态标记pachFlag(diff算法增加了一个静态标记,只对比有标记的dom元素)、事件增加缓存、静态提升(对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用)等,可以有效跳过大量diff过程; - 打包时更好的支持
tree-shaking,因此整体体积更小,加载更快 ssr渲染以字符串方式渲染
1. diff 方法优化
Vue 2x中的虚拟dom是进行全量的对比。Vue 3x中新增了静态标记(PatchFlag):在与上次虚拟结点进行对比的时候,值对比 带有patch flag的节点,并且可以通过flag的信息得知当前节点要对比的具体内容化
Vue2.x的diff算法
vue2.x的diff算法叫做全量比较,顾名思义,就是当数据改变的时候,会从头到尾的进行vDom对比,即使有些内容是永恒固定不变的

Vue3.0的diff算法
vue3.0的diff算法有个叫静态标记(PatchFlag)的小玩意,啥是静态标记呢?简单点说,就是如果你的内容会变,我会给你一个flag,下次数据更新的时候我直接来对比你,我就不对比那些没有标记的了

export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [ _createVNode("p", null, "'HelloWorld'"), _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) //上面这个1就是静态标记 ])) }@程序员poetry: 代码已经复制到剪贴板
那么肯定有人又会问了,为啥是个1呢?
TEXT = 1 // 动态文本节点 CLASS=1<<1,1 // 2//动态class STYLE=1<<2,// 4 //动态style PROPS=1<<3,// 8 //动态属性,但不包含类名和样式 FULLPR0PS=1<<4,// 16 //具有动态key属性,当key改变时,需要进行完整的diff比较。 HYDRATE_ EVENTS = 1 << 5,// 32 //带有监听事件的节点 STABLE FRAGMENT = 1 << 6, // 64 //一个不会改变子节点顺序的fragment KEYED_ FRAGMENT = 1 << 7, // 128 //带有key属性的fragment 或部分子字节有key UNKEYED FRAGMENT = 1<< 8, // 256 //子节点没有key 的fragment NEED PATCH = 1 << 9, // 512 //一个节点只会进行非props比较 DYNAMIC_SLOTS = 1 << 10 // 1024 // 动态slot HOISTED = -1 // 静态节点 // 指示在diff算法中退出优化模式 BALL = -2@程序员poetry: 代码已经复制到剪贴板
2. hoistStatic 静态提升
Vue 2x: 无论元素是否参与更新,每次都会重新创建。Vue 3x: 对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用
开启静态提升前
export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [ _createVNode("p", null, "'HelloWorld'"), _createVNode("p", null, "'HelloWorld'"), _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) ])) }@程序员poetry: 代码已经复制到剪贴板
开启静态提升后编译结果
const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */) const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */) export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [ _hoisted_1, _hoisted_2, _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */) ])) }@程序员poetry: 代码已经复制到剪贴板
可以看到开启了静态提升后,直接将那两个内容为helloworld的p标签声明在外面了,直接就拿来用了,这么搞的话那肯定会快啊
3. cacheHandlers 事件侦听器缓存
- 默认情况下
onClick会被视为动态绑定,所以每次都会去追踪它的变化 - 但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可
<div> <button @click = 'onClick'>点我</button> </div>@程序员poetry: 代码已经复制到剪贴板
开启事件侦听器缓存之前:
export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [ _createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"]) // PROPS=1<<3,// 8 //动态属性,但不包含类名和样式 ])) })@程序员poetry: 代码已经复制到剪贴板
这里有一个8,表示着这个节点有了静态标记,有静态标记就会进行diff算法对比差异,所以会浪费时间
开启事件侦听器缓存之后:
export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createBlock("div", null, [ _createVNode("button", { onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args))) }, "点我") ])) }@程序员poetry: 代码已经复制到剪贴板
# Composition API 与 Options API 有什么不同
分析
Vue3最重要更新之一就是Composition API,它具有一些列优点,其中不少是针对Options API暴露的一些问题量身打造。是Vue3推荐的写法,因此掌握好Composition API应用对掌握好Vue3至关重要

What is Composition API? (opens new window)
Composition API出现就是为了解决Options API导致相同功能代码分散的现象

体验
Composition API能更好的组织代码,下面用composition api可以提取为useCount(),用于组合、复用

compositon api提供了以下几个函数:
setuprefreactivewatchEffectwatchcomputedtoRefs- 生命周期的
hooks
回答范例
Composition API是一组API,包括:Reactivity API、生命周期钩子、依赖注入,使用户可以通过导入函数方式编写vue组件。而Options API则通过声明组件选项的对象形式编写组件Composition API最主要作用是能够简洁、高效复用逻辑。解决了过去Options API中mixins的各种缺点;另外Composition API具有更加敏捷的代码组织能力,很多用户喜欢Options API,认为所有东西都有固定位置的选项放置代码,但是单个组件增长过大之后这反而成为限制,一个逻辑关注点分散在组件各处,形成代码碎片,维护时需要反复横跳,Composition API则可以将它们有效组织在一起。最后Composition API拥有更好的类型推断,对ts支持更友好,Options API在设计之初并未考虑类型推断因素,虽然官方为此做了很多复杂的类型体操,确保用户可以在使用Options API时获得类型推断,然而还是没办法用在mixins和provide/inject上Vue3首推Composition API,但是这会让我们在代码组织上多花点心思,因此在选择上,如果我们项目属于中低复杂度的场景,Options API仍是一个好选择。对于那些大型,高扩展,强维护的项目上,Composition API会获得更大收益
可能的追问
Composition API能否和Options API一起使用?
可以在同一个组件中使用两个script标签,一个使用vue3,一个使用vue2写法,一起使用没有问题
<!-- vue3 --> <script setup> // vue3写法 </script> <!-- 降级vue2 --> <script> export default { data() {}, methods: {} } </script>@程序员poetry: 代码已经复制到剪贴板
# ref和reactive异同
这是Vue3数据响应式中非常重要的两个概念,跟我们写代码关系也很大
const count = ref(0) console.log(count.value) // 0 count.value++ console.log(count.value) // 1 const obj = reactive({ count: 0 }) obj.count++@程序员poetry: 代码已经复制到剪贴板
ref接收内部值(inner value)返回响应式Ref对象,reactive返回响应式代理对象- 从定义上看
ref通常用于处理单值的响应式,reactive用于处理对象类型的数据响应式 - 两者均是用于构造响应式数据,但是
ref主要解决原始值的响应式问题 ref返回的响应式数据在JS中使用需要加上.value才能访问其值,在视图中使用会自动脱ref,不需要.value;ref可以接收对象或数组等非原始值,但内部依然是reactive实现响应式;reactive内部如果接收Ref对象会自动脱ref;使用展开运算符(...)展开reactive返回的响应式对象会使其失去响应性,可以结合toRefs()将值转换为Ref对象之后再展开。reactive内部使用Proxy代理传入对象并拦截该对象各种操作,从而实现响应式。ref内部封装一个RefImpl类,并设置get value/set value,拦截用户对值的访问,从而实现响应式
# Vue3.2 setup 语法糖汇总
提示:vue3.2 版本开始才能使用语法糖!
在 Vue3.0 中变量必须 return 出来, template 中才能使用;而在 Vue3.2 中只需要在 script 标签上加上 setup 属性,无需 return, template 便可直接使用,非常的香啊!
1. 如何使用setup语法糖
只需在 script 标签上写上 setup
<template> </template> <script setup> </script> <style scoped lang="less"> </style>@程序员poetry: 代码已经复制到剪贴板
2. data数据的使用
由于 setup 不需写 return ,所以直接声明数据即可
<script setup> import { ref, reactive, toRefs, } from 'vue' const data = reactive({ patternVisible: false, debugVisible: false, aboutExeVisible: false, }) const content = ref('content') //使用toRefs解构 const { patternVisible, debugVisible, aboutExeVisible } = toRefs(data) </script>@程序员poetry: 代码已经复制到剪贴板
3. method方法的使用
<template > <button @click="onClickHelp">帮助</button> </template> <script setup> import {reactive} from 'vue' const data = reactive({ aboutExeVisible: false, }) // 点击帮助 const onClickHelp = () => { console.log(`帮助`) data.aboutExeVisible = true } </script>@程序员poetry: 代码已经复制到剪贴板
4. watchEffect的使用
<script setup> import { ref, watchEffect, } from 'vue' let sum = ref(0) watchEffect(()=>{ const x1 = sum.value console.log('watchEffect所指定的回调执行了') }) </script>@程序员poetry: 代码已经复制到剪贴板
5. watch的使用
<script setup> import { reactive, watch, } from 'vue' //数据 let sum = ref(0) let msg = ref('hello') let person = reactive({ name:'张三', age:18, job:{ j1:{ salary:20 } } }) // 两种监听格式 watch([sum,msg],(newValue,oldValue)=>{ console.log('sum或msg变了',newValue,oldValue) }, {immediate:true} ) watch(()=>person.job,(newValue,oldValue)=>{ console.log('person的job变化了',newValue,oldValue) },{deep:true}) </script>@程序员poetry: 代码已经复制到剪贴板
6. computed计算属性的使用
computed 计算属性有两种写法(简写和考虑读写的完整写法)
<script setup> import { reactive, computed, } from 'vue' // 数据 let person = reactive({ firstName:'poetry', lastName:'x' }) // 计算属性简写 person.fullName = computed(()=>{ return person.firstName + '-' + person.lastName }) // 完整写法 person.fullName = computed({ get(){ return person.firstName + '-' + person.lastName }, set(value){ const nameArr = value.split('-') person.firstName = nameArr[0] person.lastName = nameArr[1] } }) </script>@程序员poetry: 代码已经复制到剪贴板
7. props父子传值的使用
父组件代码如下(示例):
<template> <child :name='name'/> </template> <script setup> import {ref} from 'vue' // 引入子组件 import child from './child.vue' let name= ref('poetry') </script>@程序员poetry: 代码已经复制到剪贴板
子组件代码如下(示例):
<template> <span>{{props.name}}</span> </template> <script setup> import { defineProps } from 'vue' // 声明props const props = defineProps({ name: { type: String, default: 'poetries' } }) // 或者 //const props = defineProps(['name']) </script>@程序员poetry: 代码已经复制到剪贴板
8. emit子父传值的使用
父组件代码如下(示例):
<template> <AdoutExe @aboutExeVisible="aboutExeHandleCancel" /> </template> <script setup> import { reactive } from 'vue' // 导入子组件 import AdoutExe from '../components/AdoutExeCom' const data = reactive({ aboutExeVisible: false, }) // content组件ref // 关于系统隐藏 const aboutExeHandleCancel = () => { data.aboutExeVisible = false } </script>@程序员poetry: 代码已经复制到剪贴板
子组件代码如下(示例):
<template> <a-button @click="isOk"> 确定 </a-button> </template> <script setup> import { defineEmits } from 'vue'; // emit const emit = defineEmits(['aboutExeVisible']) /** * 方法 */ // 点击确定按钮 const isOk = () => { emit('aboutExeVisible'); } </script>@程序员poetry: 代码已经复制到剪贴板
9. 获取子组件ref变量和defineExpose暴露
即vue2中的获取子组件的ref,直接在父组件中控制子组件方法和变量的方法
父组件代码如下(示例):
<template> <button @click="onClickSetUp">点击</button> <Content ref="content" /> </template> <script setup> import {ref} from 'vue' // content组件ref const content = ref('content') // 点击设置 const onClickSetUp = ({ key }) => { content.value.modelVisible = true } </script> <style scoped lang="less"> </style>@程序员poetry: 代码已经复制到剪贴板
子组件代码如下(示例):
<template> <p>{{data }}</p> </template> <script setup> import { reactive, toRefs } from 'vue' /** * 数据部分 * */ const data = reactive({ modelVisible: false, historyVisible: false, reportVisible: false, }) defineExpose({ ...toRefs(data), }) </script>@程序员poetry: 代码已经复制到剪贴板
10. 路由useRoute和useRouter的使用
<script setup> import { useRoute, useRouter } from 'vue-router' // 声明 const route = useRoute() const router = useRouter() // 获取query console.log(route.query) // 获取params console.log(route.params) // 路由跳转 router.push({ path: `/index` }) </script>@程序员poetry: 代码已经复制到剪贴板
11. store仓库的使用
<script setup> import { useStore } from 'vuex' import { num } from '../store/index' const store = useStore(num) // 获取Vuex的state console.log(store.state.number) // 获取Vuex的getters console.log(store.state.getNumber) // 提交mutations store.commit('fnName') // 分发actions的方法 store.dispatch('fnName') </script>@程序员poetry: 代码已经复制到剪贴板
12. await的支持
setup语法糖中可直接使用await,不需要写async,setup会自动变成async setup
<script setup> import api from '../api/Api' const data = await Api.getData() console.log(data) </script>@程序员poetry: 代码已经复制到剪贴板
13. provide 和 inject 祖孙传值
父组件代码如下(示例):
<template> <AdoutExe /> </template> <script setup> import { ref,provide } from 'vue' import AdoutExe from '@/components/AdoutExeCom' let name = ref('py') // 使用provide provide('provideState', { name, changeName: () => { name.value = 'poetries' } }) </script>@程序员poetry: 代码已经复制到剪贴板
子组件代码如下(示例):
<script setup> import { inject } from 'vue' const provideState = inject('provideState') provideState.changeName() </script>@程序员poetry: 代码已经复制到剪贴板
# 34 Vue中v-html会导致哪些问题
- 可能会导致
xss攻击 v-html会替换掉标签内部的子元素
let template = require('vue-template-compiler'); let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`) // with(this){return _c('div',{domProps: {"innerHTML":_s('<span>hello</span>')}})} console.log(r.render); // _c 定义在core/instance/render.js // _s 定义在core/instance/render-helpers/index,js if (key === 'textContent' || key === 'innerHTML') { if (vnode.children) vnode.children.length = 0 if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property if (elm.childNodes.length === 1) { elm.removeChild(elm.childNodes[0]) } }@程序员poetry: 代码已经复制到剪贴板
# 35 说下$attrs和$listeners的使用场景
API考察,但$attrs和$listeners是比较少用的边界知识,而且vue3有变化,$listeners已经移除,还是有细节可说的
体验
一个包含组件透传属性的对象
<template> <child-component v-bind="$attrs"> 将非属性特性透传给内部的子组件 </child-component> </template>@程序员poetry: 代码已经复制到剪贴板
回答范例
- 我们可能会有一些属性和事件没有在
props中定义,这类称为非属性特性,结合v-bind指令可以直接透传给内部的子组件。 - 这类“属性透传”常常用于包装高阶组件时往内部传递属性,常用于爷孙组件之间传参。比如我在扩展A组件时创建了组件B组件,然后在C组件中使用B,此时传递给C的属性中只有
props里面声明的属性是给B使用的,其他的都是A需要的,此时就可以利用v-bind="$attrs"透传下去。 - 最常见用法是结合
v-bind做展开;$attrs本身不是响应式的,除非访问的属性本身是响应式对象。 vue2中使用listeners获取事件,vue3中已移除,均合并到attrs中,使用起来更简单了
原理
查看透传属性foo和普通属性bar,发现vnode结构完全相同,这说明vue3中将分辨两者工作由框架完成而非用户指定:
<template> <h1>{{ msg }}</h1> <comp foo="foo" bar="bar" /> </template>@程序员poetry: 代码已经复制到剪贴板
<template> <div> {{$attrs.foo}} {{bar}} </div> </template> <script setup> defineProps({ bar: String }) </script>@程序员poetry: 代码已经复制到剪贴板
_createVNode(Comp, { foo: "foo", bar: "bar" })@程序员poetry: 代码已经复制到剪贴板
# 36 在Vue中使用插件的步骤
- 采用
ES6的import ... from ...语法或CommonJS的require()方法引入插件 - 使用全局方法
Vue.use( plugin )使用插件,可以传入一个选项对象Vue.use(MyPlugin, { someOption: true })
# 37 vue-cli 工程技术集合介绍
# 构建的 vue-cli 工程都到了哪些技术,它们的作用分别是什么
vue.js:vue-cli工程的核心,主要特点是 双向数据绑定 和 组件系统。vue-router:vue官方推荐使用的路由框架。vuex:专为Vue.js应用项目开发的状态管理器,主要用于维护vue组件间共用的一些 变量 和 方法。axios( 或者fetch、ajax):用于发起GET、或POST等http请求,基于Promise设计。vuex等:一个专为vue设计的移动端UI组件库。- 创建一个
emit.js文件,用于vue事件机制的管理。 webpack:模块加载和vue-cli工程打包器。
# vue-cli 工程常用的 npm 命令有哪些
- 下载
node_modules资源包的命令:
npm install@程序员poetry: 代码已经复制到剪贴板
- 启动
vue-cli开发环境的 npm命令:
npm run dev@程序员poetry: 代码已经复制到剪贴板
vue-cli生成 生产环境部署资源 的npm命令:
npm run build@程序员poetry: 代码已经复制到剪贴板
- 用于查看
vue-cli生产环境部署资源文件大小的npm命令:
npm run build --report@程序员poetry: 代码已经复制到剪贴板
在浏览器上自动弹出一个 展示
vue-cli工程打包后app.js、manifest.js、vendor.js文件里面所包含代码的页面。可以具此优化vue-cli生产环境部署的静态资源,提升 页面 的加载速度
# 请说出vue cli项目中src目录每个文件夹和文件的用法
assets文件夹是放静态资源;components是放组件;router是定义路由相关的配置;view视图;app.vue是一个应用主组件;main.js是入口文件
# 38 delete和Vue.delete删除数组的区别?
delete只是被删除的元素变成了empty/undefined其他的元素的键值还是不变。Vue.delete直接删除了数组 改变了数组的键值。
var a=[1,2,3,4] var b=[1,2,3,4] delete a[0] console.log(a) //[empty,2,3,4] this.$delete(b,0) console.log(b) //[2,3,4]@程序员poetry: 代码已经复制到剪贴板
# 39 v-on可以监听多个方法吗?
可以监听多个方法
<input type="text" :value="name" @input="onInput" @focus="onFocus" @blur="onBlur" />@程序员poetry: 代码已经复制到剪贴板
v-on 常用修饰符
.stop该修饰符将阻止事件向上冒泡。同理于调用event.stopPropagation()方法.prevent该修饰符会阻止当前事件的默认行为。同理于调用event.preventDefault()方法.self该指令只当事件是从事件绑定的元素本身触发时才触发回调.once该修饰符表示绑定的事件只会被触发一次
# 40 v-once的使用场景有哪些
分析
v-once是Vue中内置指令,很有用的API,在优化方面经常会用到
体验
仅渲染元素和组件一次,并且跳过未来更新
<!-- single element --> <span v-once>This will never change: {{msg}}</span> <!-- the element have children --> <div v-once> <h1>comment</h1> <p>{{msg}}</p> </div> <!-- component --> <my-component v-once :comment="msg"></my-component> <!-- `v-for` directive --> <ul> <li v-for="i in list" v-once>{{i}}</li> </ul>@程序员poetry: 代码已经复制到剪贴板
回答范例
v-once是vue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新- 如果我们有一些元素或者组件在初始化渲染之后不再需要变化,这种情况下适合使用
v-once,这样哪怕这些数据变化,vue也会跳过更新,是一种代码优化手段 - 我们只需要作用的组件或元素上加上
v-once即可 vue3.2之后,又增加了v-memo指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了- 编译器发现元素上面有
v-once时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算
原理
下面例子使用了v-once:
<script setup> import { ref } from 'vue' const msg = ref('Hello World!') </script> <template> <h1 v-once>{{ msg }}</h1> <input v-model="msg"> </template>@程序员poetry: 代码已经复制到剪贴板
我们发现v-once出现后,编译器会缓存作用元素或组件,从而避免以后更新时重新计算这一部分:
// ... return (_ctx, _cache) => { return (_openBlock(), _createElementBlock(_Fragment, null, [ // 从缓存获取vnode _cache[0] || ( _setBlockTracking(-1), _cache[0] = _createElementVNode("h1", null, [ _createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */) ]), _setBlockTracking(1), _cache[0] ), // ...@程序员poetry: 代码已经复制到剪贴板
# 41 Vue Ref的作用
- 获取
dom元素this.$refs.box - 获取子组件中的
datathis.$refs.box.msg - 调用子组件中的方法
this.$refs.box.open()
# 42 scoped样式穿透
scoped虽然避免了组件间样式污染,但是很多时候我们需要修改组件中的某个样式,但是又不想去除scoped属性
- 使用
/deep/
<!-- Parent --> <template> <div class="wrap"> <Child /> </div> </template> <style lang="scss" scoped> .wrap /deep/ .box{ background: red; } </style> <!-- Child --> <template> <div class="box"></div> </template>@程序员poetry: 代码已经复制到剪贴板
- 使用两个
style标签
<!-- Parent --> <template> <div class="wrap"> <Child /> </div> </template> <style lang="scss" scoped> /* 其他样式 */ </style> <style lang="scss"> .wrap .box{ background: red; } </style> <!-- Child --> <template> <div class="box"></div> </template>@程序员poetry: 代码已经复制到剪贴板
# 43 Class 与 Style 如何动态绑定
Class 可以通过对象语法和数组语法进行动态绑定
对象语法:
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div> data: { isActive: true, hasError: false }@程序员poetry: 代码已经复制到剪贴板
数组语法:
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div> data: { activeClass: 'active', errorClass: 'text-danger' }@程序员poetry: 代码已经复制到剪贴板
Style 也可以通过对象语法和数组语法进行动态绑定
对象语法:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div> data: { activeColor: 'red', fontSize: 30 }@程序员poetry: 代码已经复制到剪贴板
数组语法:
<div v-bind:style="[styleColor, styleSize]"></div> data: { styleColor: { color: 'red' }, styleSize:{ fontSize:'23px' } }@程序员poetry: 代码已经复制到剪贴板
# 44 Vue为什么没有类似于React中shouldComponentUpdate的生命周期
- 考点:
Vue的变化侦测原理 - 前置知识: 依赖收集、虚拟
DOM、响应式系统
根本原因是
Vue与React的变化侦测方式有所不同
- 当React知道发生变化后,会使用
Virtual Dom Diff进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要shouldComponentUpdate进行手动操作来减少diff,从而提高程序整体的性能 Vue在一开始就知道那个组件发生了变化,不需要手动控制diff,而组件内部采用的diff方式实际上是可以引入类似于shouldComponentUpdate相关生命周期的,但是通常合理大小的组件不会有过量的diff,手动优化的价值有限,因此目前Vue并没有考虑引入shouldComponentUpdate这种手动优化的生命周期
# 45 SPA、SSR的区别是什么
我们现在编写的Vue、React和Angular应用大多数情况下都会在一个页面中,点击链接跳转页面通常是内容切换而非页面跳转,由于良好的用户体验逐渐成为主流的开发模式。但同时也会有首屏加载时间长,SEO不友好的问题,因此有了SSR,这也是为什么面试中会问到两者的区别
SPA(Single Page Application)即单页面应用。一般也称为 客户端渲染(Client Side Render), 简称CSR。SSR(Server Side Render)即 服务端渲染。一般也称为 多页面应用(Mulpile Page Application),简称MPASPA应用只会首次请求html文件,后续只需要请求JSON数据即可,因此用户体验更好,节约流量,服务端压力也较小。但是首屏加载的时间会变长,而且SEO不友好。为了解决以上缺点,就有了SSR方案,由于HTML内容在服务器一次性生成出来,首屏加载快,搜索引擎也可以很方便的抓取页面信息。但同时SSR方案也会有性能,开发受限等问题- 在选择上,如果我们的应用存在首屏加载优化需求,
SEO需求时,就可以考虑SSR - 但并不是只有这一种替代方案,比如对一些不常变化的静态网站,SSR反而浪费资源,我们可以考虑预渲染(
prerender)方案。另外nuxt.js/next.js中给我们提供了SSG(Static Site Generate)静态网站生成方案也是很好的静态站点解决方案,结合一些CI手段,可以起到很好的优化效果,且能节约服务器资源
内容生成上的区别:
SSR

SPA

部署上的区别

# 46 vue-loader是什么?它有什么作用?
回答范例
vue-loader是用于处理单文件组件(SFC,Single-File Component)的webpack loader- 因为有了
vue-loader,我们就可以在项目中编写SFC格式的Vue组件,我们可以把代码分割为<template>、<script>和<style>,代码会异常清晰。结合其他loader我们还可以用Pug编写<template>,用SASS编写<style>,用TS编写<script>。我们的<style>还可以单独作用当前组件 webpack打包时,会以loader的方式调用vue-loadervue-loader被执行时,它会对SFC中的每个语言块用单独的loader链处理。最后将这些单独的块装配成最终的组件模块
原理
vue-loader会调用@vue/compiler-sfc模块解析SFC源码为一个描述符(Descriptor),然后为每个语言块生成import代码,返回的代码类似下面
// source.vue被vue-loader处理之后返回的代码 // import the <template> block import render from 'source.vue?vue&type=template' // import the <script> block import script from 'source.vue?vue&type=script' export * from 'source.vue?vue&type=script' // import <style> blocks import 'source.vue?vue&type=style&index=1' script.render = render export default script@程序员poetry: 代码已经复制到剪贴板
我们想要script块中的内容被作为js处理(当然如果是<script lang="ts">被作为ts理),这样我们想要webpack把配置中跟.js匹配的规则都应用到形如source.vue?vue&type=script的这个请求上。例如我们对所有*.js配置了babel-loader,这个规则将被克隆并应用到所在Vue SFC
import script from 'source.vue?vue&type=script@程序员poetry: 代码已经复制到剪贴板
将被展开为:
import script from 'babel-loader!vue-loader!source.vue?vue&type=script'@程序员poetry: 代码已经复制到剪贴板
类似的,如果我们对.sass文件配置了style-loader + css-loader + sass-loader,对下面的代码
<style scoped lang="scss">@程序员poetry: 代码已经复制到剪贴板
vue-loader将会返回给我们下面结果:
import 'source.vue?vue&type=style&index=1&scoped&lang=scss'@程序员poetry: 代码已经复制到剪贴板
然后webpack会展开如下:
import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'@程序员poetry: 代码已经复制到剪贴板
- 当处理展开请求时,
vue-loader将被再次调用。这次,loader将会关注那些有查询串的请求,且仅针对特定块,它会选中特定块内部的内容并传递给后面匹配的loader - 对于
<script>块,处理到这就可以了,但是<template>和<style>还有一些额外任务要做,比如- 需要用
Vue模板编译器编译template,从而得到render函数 - 需要对
<style scoped>中的CSS做后处理(post-process),该操作在css-loader之后但在style-loader之前
- 需要用
实现上这些附加的loader需要被注入到已经展开的loader链上,最终的请求会像下面这样:
// <template lang="pug"> import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template' // <style scoped lang="scss"> import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'@程序员poetry: 代码已经复制到剪贴板
# 47 从0到1自己构架一个vue项目,说说有哪些步骤、哪些重要插件、目录结构你会怎么组织
综合实践类题目,考查实战能力。没有什么绝对的正确答案,把平时工作的重点有条理的描述一下即可
思路
- 构建项目,创建项目基本结构
- 引入必要的插件:
- 代码规范:
prettier,eslint - 提交规范:
husky,lint-staged` - 其他常用:
svg-loader,vueuse,nprogress - 常见目录结构
回答范例
- 从
0创建一个项目我大致会做以下事情:项目构建、引入必要插件、代码规范、提交规范、常用库和组件 - 目前
vue3项目我会用vite或者create-vue创建项目 - 接下来引入必要插件:路由插件
vue-router、状态管理vuex/pinia、ui库我比较喜欢element-plus和antd-vue、http工具我会选axios - 其他比较常用的库有
vueuse,nprogress,图标可以使用vite-svg-loader - 下面是代码规范:结合
prettier和eslint即可 - 最后是提交规范,可以使用
husky,lint-staged,commitlint - 目录结构我有如下习惯:
.vscode:用来放项目中的vscode配置
plugins:用来放vite插件的plugin配置public:用来放一些诸如 页头icon之类的公共文件,会被打包到dist根目录下src:用来放项目代码文件api:用来放http的一些接口配置assets:用来放一些CSS之类的静态资源components:用来放项目通用组件layout:用来放项目的布局router:用来放项目的路由配置store:用来放状态管理Pinia的配置utils:用来放项目中的工具方法类views:用来放项目的页面文件
# 48 实际工作中,你总结的vue最佳实践有哪些
从编码风格、性能、安全等方面说几条:
编码风格方面:
- 命名组件时使用“多词”风格避免和
HTML元素冲突 - 使用“细节化”方式定义属性而不是只有一个属性名
- 属性名声明时使用“驼峰命名”,模板或
jsx中使用“肉串命名” - 使用
v-for时务必加上key,且不要跟v-if写在一起
性能方面:
- 路由懒加载减少应用尺寸
- 利用
SSR减少首屏加载时间 - 利用
v-once渲染那些不需要更新的内容 - 一些长列表可以利用虚拟滚动技术避免内存过度占用
- 对于深层嵌套对象的大数组可以使用
shallowRef或shallowReactive降低开销 - 避免不必要的组件抽象
安全:
- 不使用不可信模板,例如使用用户输入拼接模板:
template: <div> + userProvidedString + </div> - 避免使用
v-html,:url,:style等,避免html、url、样式等注入
# 49 vue 中使用了哪些设计模式
- 工厂模式 传入参数即可创建实例:虚拟
DOM根据参数的不同返回基础标签的Vnode和组件Vnode - 单例模式 整个程序有且仅有一个实例:
vuex和vue-router的插件注册方法install判断如果系统存在实例就直接返回掉 - 发布-订阅模式 (vue 事件机制)
- 观察者模式 (响应式数据原理)
- 装饰模式: (@装饰器的用法)
- 策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略
# 50 如果让你从零开始写一个vuex,说说你的思路
思路分析
这个题目很有难度,首先思考vuex解决的问题:存储用户全局状态并提供管理状态API。
vuex需求分析- 如何实现这些需求
回答范例
- 官方说
vuex是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuex
- 要实现一个
Store存储全局状态 - 要提供修改状态所需API:
commit(type, payload), dispatch(type, payload)
- 实现
Store时,可以定义Store类,构造函数接收选项options,设置属性state对外暴露状态,提供commit和dispatch修改属性state。这里需要设置state为响应式对象,同时将Store定义为一个Vue插件 commit(type, payload)方法中可以获取用户传入mutations并执行它,这样可以按用户提供的方法修改状态。dispatch(type, payload)类似,但需要注意它可能是异步的,需要返回一个Promise给用户以处理异步结果
实践
Store的实现:
class Store { constructor(options) { this.state = reactive(options.state) this.options = options } commit(type, payload) { this.options.mutations[type].call(this, this.state, payload) } }@程序员poetry: 代码已经复制到剪贴板
vuex简易版
/** * 1 实现插件,挂载$store * 2 实现store */ let Vue; class Store { constructor(options) { // state响应式处理 // 外部访问: this.$store.state.*** // 第一种写法 // this.state = new Vue({ // data: options.state // }) // 第二种写法:防止外界直接接触内部vue实例,防止外部强行变更 this._vm = new Vue({ data: { $$state: options.state } }) this._mutations = options.mutations this._actions = options.actions this.getters = {} options.getters && this.handleGetters(options.getters) this.commit = this.commit.bind(this) this.dispatch = this.dispatch.bind(this) } get state () { return this._vm._data.$$state } set state (val) { return new Error('Please use replaceState to reset state') } handleGetters (getters) { Object.keys(getters).map(key => { Object.defineProperty(this.getters, key, { get: () => getters[key](this.state) }) }) } commit (type, payload) { let entry = this._mutations[type] if (!entry) { return new Error(`${type} is not defined`) } entry(this.state, payload) } dispatch (type, payload) { let entry = this._actions[type] if (!entry) { return new Error(`${type} is not defined`) } entry(this, payload) } } const install = (_Vue) => { Vue = _Vue Vue.mixin({ beforeCreate () { if (this.$options.store) { Vue.prototype.$store = this.$options.store } }, }) } export default { Store, install }@程序员poetry: 代码已经复制到剪贴板
验证方式
import Vue from 'vue' import Vuex from './vuex' // this.$store Vue.use(Vuex) export default new Vuex.Store({ state: { counter: 0 }, mutations: { // state从哪里来的 add (state) { state.counter++ } }, getters: { doubleCounter (state) { return state.counter * 2 } }, actions: { add ({ commit }) { setTimeout(() => { commit('add') }, 1000) } }, modules: { } })@程序员poetry: 代码已经复制到剪贴板
# 51 使用vue渲染大量数据时应该怎么优化?说下你的思路!
分析
企业级项目中渲染大量数据的情况比较常见,因此这是一道非常好的综合实践题目。
回答
- 在大型企业级项目中经常需要渲染大量数据,此时很容易出现卡顿的情况。比如大数据量的表格、树
- 处理时要根据情况做不同处理:
- 可以采取分页的方式获取,避免渲染大量数据
- vue-virtual-scroller (opens new window)等虚拟滚动方案,只渲染视口范围内的数据
- 如果不需要更新,可以使用v-once方式只渲染一次
- 通过v-memo (opens new window)可以缓存结果,结合
v-for使用,避免数据变化时不必要的VNode创建 - 可以采用懒加载方式,在用户需要的时候再加载数据,比如
tree组件子树的懒加载
- 还是要看具体需求,首先从设计上避免大数据获取和渲染;实在需要这样做可以采用虚表的方式优化渲染;最后优化更新,如果不需要更新可以
v-once处理,需要更新可以v-memo进一步优化大数据更新性能。其他可以采用的是交互方式优化,无线滚动、懒加载等方案
# 52 你是怎么处理vue项目中的错误的?
分析
- 这是一个综合应用题目,在项目中我们常常需要将App的异常上报,此时错误处理就很重要了。
- 这里要区分错误的类型,针对性做收集。
- 然后是将收集的的错误信息上报服务器。
思路
- 首先区分错误类型
- 根据错误不同类型做相应收集
- 收集的错误是如何上报服务器的
回答范例
- 应用中的错误类型分为"
接口异常"和“代码逻辑异常” - 我们需要根据不同错误类型做相应处理:接口异常是我们请求后端接口过程中发生的异常,可能是请求失败,也可能是请求获得了服务器响应,但是返回的是错误状态。以
Axios为例,这类异常我们可以通过封装Axios,在拦截器中统一处理整个应用中请求的错误。代码逻辑异常是我们编写的前端代码中存在逻辑上的错误造成的异常,vue应用中最常见的方式是使用全局错误处理函数app.config.errorHandler收集错误 - 收集到错误之后,需要统一处理这些异常:分析错误,获取需要错误信息和数据。这里应该有效区分错误类型,如果是请求错误,需要上报接口信息,参数,状态码等;对于前端逻辑异常,获取错误名称和详情即可。另外还可以收集应用名称、环境、版本、用户信息,所在页面等。这些信息可以通过
vuex存储的全局状态和路由信息获取
实践
axios拦截器中处理捕获异常:
// 响应拦截器 instance.interceptors.response.use( (response) => { return response.data; }, (error) => { // 存在response说明服务器有响应 if (error.response) { let response = error.response; if (response.status >= 400) { handleError(response); } } else { handleError(null); } return Promise.reject(error); }, );@程序员poetry: 代码已经复制到剪贴板
vue中全局捕获异常:
import { createApp } from 'vue' const app = createApp(...) app.config.errorHandler = (err, instance, info) => { // report error to tracking services }@程序员poetry: 代码已经复制到剪贴板
处理接口请求错误:
function handleError(error, type) { if(type == 1) { // 接口错误,从config字段中获取请求信息 let { url, method, params, data } = error.config let err_data = { url, method, params: { query: params, body: data }, error: error.data?.message || JSON.stringify(error.data), }) } }@程序员poetry: 代码已经复制到剪贴板
处理前端逻辑错误:
function handleError(error, type) { if(type == 2) { let errData = null // 逻辑错误 if(error instanceof Error) { let { name, message } = error errData = { type: name, error: message } } else { errData = { type: 'other', error: JSON.strigify(error) } } } }@程序员poetry: 代码已经复制到剪贴板
# 53 Vue中常见性能优化
编码优化:
- 使用
v-show复用DOM:避免重复创建组件
<template> <div class="cell"> <!-- 这种情况用v-show复用DOM,比v-if效果好 --> <div v-show="value" class="on"> <Heavy :n="10000"/> </div> <section v-show="!value" class="off"> <Heavy :n="10000"/> </section> </div> </template>@程序员poetry: 代码已经复制到剪贴板
- 合理使用路由懒加载、异步组件,有效拆分
App尺寸,访问时才异步加载
const router = createRouter({ routes: [ // 借助webpack的import()实现异步组件 { path: '/foo', component: () => import('./Foo.vue') } ] })@程序员poetry: 代码已经复制到剪贴板
keep-alive缓存页面:避免重复创建组件实例,且能保留缓存组件状态
<router-view v-slot="{ Component }"> <keep-alive> <component :is="Component"></component> </keep-alive> </router-view>@程序员poetry: 代码已经复制到剪贴板
v-once和v-memo:不再变化的数据使用v-once
<!-- single element --> <span v-once>This will never change: {{msg}}</span> <!-- the element have children --> <div v-once> <h1>comment</h1> <p>{{msg}}</p> </div> <!-- component --> <my-component v-once :comment="msg"></my-component> <!-- `v-for` directive --> <ul> <li v-for="i in list" v-once>{{i}}</li> </ul>@程序员poetry: 代码已经复制到剪贴板
按条件跳过更新时使用v-momo:下面这个列表只会更新选中状态变化项
<div v-for="item in list" :key="item.id" v-memo="[item.id === selected]"> <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p> <p>...more child nodes</p> </div>@程序员poetry: 代码已经复制到剪贴板
- 长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容
<recycle-scroller class="items" :items="items" :item-size="24" > <template v-slot="{ item }"> <FetchItemView :item="item" @vote="voteItem(item)" /> </template> </recycle-scroller>@程序员poetry: 代码已经复制到剪贴板
- 防止内部泄漏,组件销毁后把全局变量和事件销毁:
Vue组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件
export default { created() { this.timer = setInterval(this.refresh, 2000) }, beforeUnmount() { clearInterval(this.timer) } }@程序员poetry: 代码已经复制到剪贴板
- 图片懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载,等到滚动到可视区域后再去加载
<!-- 参考 https://github.com/hilongjw/vue-lazyload --> <img v-lazy="/static/img/1.png">@程序员poetry: 代码已经复制到剪贴板
- 滚动到可视区域动态加载
https://tangbc.github.io/vue-virtual-scroll-list (opens new window)
- 第三方插件按需引入:(
babel-plugin-component)
像element-plus这样的第三方组件库可以按需引入避免体积太大
import { createApp } from 'vue'; import { Button, Select } from 'element-plus'; const app = createApp() app.use(Button) app.use(Select)@程序员poetry: 代码已经复制到剪贴板
- 服务端渲染:SSR
如果SPA应用有首屏渲染慢的问题,可以考虑SSR
以及下面的其他方法
- 不要将所有的数据都放在
data中,data中的数据都会增加getter和setter,会收集对应的watcher v-for遍历为item添加keyv-for遍历避免同时使用v-if- 区分
computed和watch的使用 - 拆分组件(提高复用性、增加代码的可维护性,减少不必要的渲染 )
- 防抖、节流
用户体验
app-skeleton骨架屏pwaserviceworker
SEO优化
- 预渲染插件
prerender-spa-plugin - 服务端渲染
ssr
打包优化
Webpack对图片进行压缩- 使用
cdn的方式加载第三方模块 - 多线程打包
happypack splitChunks抽离公共文件- 优化
SourceMap - 构建结果输出分析,利用
webpack-bundle-analyzer可视化分析工具
基础的 Web 技术的优化
- 服务端
gzip压缩 - 浏览器缓存
CDN的使用- 使用
Chrome Performance查找性能瓶颈
# 54 Vue项目性能优化-详细
Vue框架通过数据双向绑定和虚拟DOM技术,帮我们处理了前端开发中最脏最累的DOM操作部分, 我们不再需要去考虑如何操作DOM以及如何最高效地操作DOM;但Vue项目中仍然存在项目首屏优化、Webpack编译配置优化等问题,所以我们仍然需要去关注Vue项目性能方面的优化,使项目具有更高效的性能、更好的用户体验
# 代码层面的优化
1. v-if 和 v-show 区分使用场景
v-if是 真正 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块v-show就简单得多, 不管初始条件是什么,元素总是会被渲染,并且只是简单地基于CSSdisplay的none/block属性进行切换。- 所以,
v-if适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show则适用于需要非常频繁切换条件的场景
2. computed 和 watch 区分使用场景
computed: 是计算属性,依赖其它属性值,并且computed的值有缓存,只有它依赖的属性值发生改变,下一次获取computed的值时才会重新计算 computed 的值;watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作
运用场景:
- 当我们需要进行数值计算,并且依赖于其它数据时,应该使用
computed,因为可以利用computed的缓存特性,避免每次获取值时,都要重新计算; - 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用
watch,使用watch选项允许我们执行异步操作 ( 访问一个API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的
3. v-for 遍历必须为 item 添加 key,且避免同时使用 v-if
v-for遍历必须为item添加key- 在列表数据进行遍历渲染时,需要为每一项
item设置唯一key值,方便Vue.js内部机制精准找到该条列表数据。当state更新时,新的状态值和旧的状态值对比,较快地定位到diff
- 在列表数据进行遍历渲染时,需要为每一项
v-for遍历避免同时使用v-ifvue2.x中v-for比v-if优先级高,如果每一次都需要遍历整个数组,将会影响速度,尤其是当之需要渲染很小一部分的时候,必要情况下应该替换成computed属性
推荐:
<ul> <li v-for="user in activeUsers" :key="user.id"> {{ user.name }} </li> </ul> computed: { activeUsers: function () { return this.users.filter(function (user) { return user.isActive }) } }@程序员poetry: 代码已经复制到剪贴板
不推荐:
<ul> <li v-for="user in users" v-if="user.isActive" :key="user.id"> {{ user.name }} </li> </ul>@程序员poetry: 代码已经复制到剪贴板
4. 长列表性能优化
Vue会通过Object.defineProperty对数据进行劫持,来实现视图响应数据的变化,然而有些时候我们的组件就是纯粹的数据展示,不会有任何改变,我们就不需要 Vue 来劫持我们的数据,在大量数据展示的情况下,这能够很明显的减少组件初始化的时间,那如何禁止Vue劫持我们的数据呢?可以通过Object.freeze方法来冻结一个对象,一旦被冻结的对象就再也不能被修改了
export default { data: () => ({ users: {} }), async created() { const users = await axios.get("/api/users"); this.users = Object.freeze(users); } };@程序员poetry: 代码已经复制到剪贴板
5. 事件的销毁
Vue 组件销毁时,会自动清理它与其它实例的连接,解绑它的全部指令及事件监听器,但是仅限于组件本身的事件。 如果在 js 内使用 addEventListener 等方式是不会自动销毁的,我们需要在组件销毁时手动移除这些事件的监听,以免造成内存泄露,如:
created() { addEventListener('click', this.click, false) }, beforeDestroy() { removeEventListener('click', this.click, false) }@程序员poetry: 代码已经复制到剪贴板
6. 图片资源懒加载
对于图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。这样对于页面加载性能上会有很大的提升,也提高了用户体验。我们在项目中使用 Vue 的 vue-lazyload 插件
npm install vue-lazyload --save-dev@程序员poetry: 代码已经复制到剪贴板
在入口文件 man.js 中引入并使用
import VueLazyload from 'vue-lazyload' Vue.use(VueLazyload) // 或者添加自定义选项 Vue.use(VueLazyload, { preLoad: 1.3, error: 'dist/error.png', loading: 'dist/loading.gif', attempt: 1 })@程序员poetry: 代码已经复制到剪贴板
在 vue 文件中将 img 标签的 src 属性直接改为 v-lazy ,从而将图片显示方式更改为懒加载显示
<img v-lazy="/static/img/1.png">@程序员poetry: 代码已经复制到剪贴板
以上为 vue-lazyload 插件的简单使用,如果要看插件的更多参数选项,可以查看 vue-lazyload 的 github 地址 (opens new window)
7. 路由懒加载
Vue 是单页面应用,可能会有很多的路由引入 ,这样使用 webpcak 打包后的文件很大,当进入首页时,加载的资源过多,页面会出现白屏的情况,不利于用户体验。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就更加高效了。这样会大大提高首屏显示的速度,但是可能其他的页面的速度就会降下来
路由懒加载:
const Foo = () => import('./Foo.vue') const router = new VueRouter({ routes: [ { path: '/foo', component: Foo } ] })@程序员poetry: 代码已经复制到剪贴板
8. 第三方插件的按需引入
我们在项目中经常会需要引入第三方插件,如果我们直接引入整个插件,会导致项目的体积太大,我们可以借助 babel-plugin-component ,然后可以只引入需要的组件,以达到减小项目体积的目的。以下为项目中引入 element-ui 组件库为例
npm install babel-plugin-component -D@程序员poetry: 代码已经复制到剪贴板
将 .babelrc 修改为:
{ "presets": [["es2015", { "modules": false }]], "plugins": [ [ "component", { "libraryName": "element-ui", "styleLibraryName": "theme-chalk" } ] ] }@程序员poetry: 代码已经复制到剪贴板
在 main.js 中引入部分组件:
import Vue from 'vue'; import { Button, Select } from 'element-ui'; Vue.use(Button) Vue.use(Select)@程序员poetry: 代码已经复制到剪贴板
9. 优化无限列表性能
如果你的应用存在非常长或者无限滚动的列表,那么需要采用虚拟列表的技术来优化性能,只需要渲染少部分区域的内容,减少重新渲染组件和创建 dom 节点的时间。 你可以参考以下开源项目 vue-virtual-scroll-list (opens new window) 和 vue-virtual-scroller (opens new window) 来优化这种无限列表的场景的
10. 服务端渲染 SSR or 预渲染
服务端渲染是指 Vue 在客户端将标签渲染成的整个 html 片段的工作在服务端完成,服务端形成的 html 片段直接返回给客户端这个过程就叫做服务端渲染。
- 如果你的项目的
SEO和首屏渲染是评价项目的关键指标,那么你的项目就需要服务端渲染来帮助你实现最佳的初始加载性能和SEO - 如果你的
Vue项目只需改善少数营销页面(例如/,/about,/contact等)的SEO,那么你可能需要预渲染,在构建时简单地生成针对特定路由的静态HTML文件。优点是设置预渲染更简单,并可以将你的前端作为一个完全静态的站点,具体你可以使用 prerender-spa-plugin (opens new window) 就可以轻松地添加预渲染
# Webpack 层面的优化
1. Webpack 对图片进行压缩
对小于 limit 的图片转化为 base64 格式,其余的不做操作。所以对有些较大的图片资源,在请求资源的时候,加载会很慢,我们可以用 image-webpack-loader来压缩图片
npm install image-webpack-loader --save-dev@程序员poetry: 代码已经复制到剪贴板
{ test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, use:[ { loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash:7].[ext]') } }, { loader: 'image-webpack-loader', options: { bypassOnDebug: true, } } ] }@程序员poetry: 代码已经复制到剪贴板
2. 减少 ES6 转为 ES5 的冗余代码
Babel 插件会在将 ES6 代码转换成 ES5 代码时会注入一些辅助函数,例如下面的 ES6 代码
class HelloWebpack extends Component{...}@程序员poetry: 代码已经复制到剪贴板
这段代码再被转换成能正常运行的 ES5 代码时需要以下两个辅助函数:
babel-runtime/helpers/createClass // 用于实现 class 语法 babel-runtime/helpers/inherits // 用于实现 extends 语法@程序员poetry: 代码已经复制到剪贴板
在默认情况下, Babel 会在每个输出文件中内嵌这些依赖的辅助函数代码,如果多个源代码文件都依赖这些辅助函数,那么这些辅助函数的代码将会出现很多次,造成代码冗余。为了不让这些辅助函数的代码重复出现,可以在依赖它们时通过 require('babel-runtime/helpers/createClass') 的方式导入,这样就能做到只让它们出现一次。babel-plugin-transform-runtime 插件就是用来实现这个作用的,将相关辅助函数进行替换成导入语句,从而减小 babel 编译出来的代码的文件大小
npm install babel-plugin-transform-runtime --save-dev@程序员poetry: 代码已经复制到剪贴板
修改 .babelrc 配置文件为:
"plugins": [ "transform-runtime" ]@程序员poetry: 代码已经复制到剪贴板
3. 提取公共代码
如果项目中没有去将每个页面的第三方库和公共模块提取出来,则项目会存在以下问题:
- 相同的资源被重复加载,浪费用户的流量和服务器的成本。
- 每个页面需要加载的资源太大,导致网页首屏加载缓慢,影响用户体验。
所以我们需要将多个页面的公共代码抽离成单独的文件,来优化以上问题 。Webpack 内置了专门用于提取多个Chunk 中的公共部分的插件 CommonsChunkPlugin,我们在项目中 CommonsChunkPlugin 的配置如下:
// 所有在 package.json 里面依赖的包,都会被打包进 vendor.js 这个文件中。 new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: function(module, count) { return ( module.resource && /\.js$/.test(module.resource) && module.resource.indexOf( path.join(__dirname, '../node_modules') ) === 0 ); } }), // 抽取出代码模块的映射关系 new webpack.optimize.CommonsChunkPlugin({ name: 'manifest', chunks: ['vendor'] })@程序员poetry: 代码已经复制到剪贴板
4. 模板预编译
- 当使用 DOM 内模板或 JavaScript 内的字符串模板时,模板会在运行时被编译为渲染函数。通常情况下这个过程已经足够快了,但对性能敏感的应用还是最好避免这种用法。
- 预编译模板最简单的方式就是使用单文件组件——相关的构建设置会自动把预编译处理好,所以构建好的代码已经包含了编译出来的渲染函数而不是原始的模板字符串。
- 如果你使用 webpack,并且喜欢分离 JavaScript 和模板文件,你可以使用 vue-template-loader (opens new window),它也可以在构建过程中把模板文件转换成为 JavaScript 渲染函数
5. 提取组件的 CSS
当使用单文件组件时,组件内的 CSS 会以 style 标签的方式通过 JavaScript 动态注入。这有一些小小的运行时开销,如果你使用服务端渲染,这会导致一段 “无样式内容闪烁 (fouc) ” 。将所有组件的 CSS 提取到同一个文件可以避免这个问题,也会让 CSS 更好地进行压缩和缓存
6. 优化 SourceMap
我们在项目进行打包后,会将开发中的多个文件代码打包到一个文件中,并且经过压缩、去掉多余的空格、babel编译化后,最终将编译得到的代码会用于线上环境,那么这样处理后的代码和源代码会有很大的差别,当有 bug的时候,我们只能定位到压缩处理后的代码位置,无法定位到开发环境中的代码,对于开发来说不好调式定位问题,因此 sourceMap 出现了,它就是为了解决不好调式代码问题的
SourceMap的可选值如下(+号越多,代表速度越快,-号越多,代表速度越慢,o代表中等速度)

- 开发环境推荐:
cheap-module-eval-source-map - 生产环境推荐:
cheap-module-source-map
原因如下:
cheap: 源代码中的列信息是没有任何作用,因此我们打包后的文件不希望包含列相关信息,只有行信息能建立打包前后的依赖关系。因此不管是开发环境或生产环境,我们都希望添加cheap的基本类型来忽略打包前后的列信息;module:不管是开发环境还是正式环境,我们都希望能定位到bug的源代码具体的位置,比如说某个Vue文件报错了,我们希望能定位到具体的Vue文件,因此我们也需要module配置;soure-map:source-map会为每一个打包后的模块生成独立的soucemap文件 ,因此我们需要增加source-map属性;eval-source-map:eval打包代码的速度非常快,因为它不生成map文件,但是可以对eval组合使用eval-source-map使用会将map文件以DataURL的形式存在打包后的js文件中。在正式环境中不要使用eval-source-map, 因为它会增加文件的大小,但是在开发环境中,可以试用下,因为他们打包的速度很快。
7. 构建结果输出分析
Webpack 输出的代码可读性非常差而且文件非常大,让我们非常头疼。为了更简单、直观地分析输出结果,社区中出现了许多可视化分析工具。这些工具以图形的方式将结果更直观地展示出来,让我们快速了解问题所在。接下来讲解我们在 Vue 项目中用到的分析工具:webpack-bundle-analyzer
if (config.build.bundleAnalyzerReport) { var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; webpackConfig.plugins.push(new BundleAnalyzerPlugin()); }@程序员poetry: 代码已经复制到剪贴板
执行 $ npm run build --report 后生成分析报告如下

# 基础的 Web 技术优化
1. 开启 gzip 压缩
gzip是GNUzip的缩写,最早用于UNIX系统的文件压缩。HTTP协议上的gzip编码是一种用来改进web应用程序性能的技术,web服务器和客户端(浏览器)必须共同支持 gzip。目前主流的浏览器,Chrome,firefox,IE等都支持该协议。常见的服务器如 Apache,Nginx,IIS 同样支持,zip压缩效率非常高,通常可以达到70%的压缩率,也就是说,如果你的网页有30K,压缩之后就变成了9K左右
以下我们以服务端使用我们熟悉的 express 为例,开启 gzip 非常简单,相关步骤如下:
npm install compression --save@程序员poetry: 代码已经复制到剪贴板
var compression = require('compression'); var app = express(); app.use(compression())@程序员poetry: 代码已经复制到剪贴板
重启服务,观察网络面板里面的 response header,如果看到如下红圈里的字段则表明 gzip 开启成功

Nginx开启gzip压缩
#是否启动gzip压缩,on代表启动,off代表开启 gzip on; #需要压缩的常见静态资源 gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png; #由于nginx的压缩发生在浏览器端而微软的ie6很坑爹,会导致压缩后图片看不见所以该选 项是禁止ie6发生压缩 gzip_disable "MSIE [1-6]\."; #如果文件大于1k就启动压缩 gzip_min_length 1k; #以16k为单位,按照原始数据的大小以4倍的方式申请内存空间,一般此项不要修改 gzip_buffers 4 16k; #压缩的等级,数字选择范围是1-9,数字越小压缩的速度越快,消耗cpu就越大 gzip_comp_level 2;@程序员poetry: 代码已经复制到剪贴板
要想配置生效,记得重启nginx服务
nginx -t nginx -s reload@程序员poetry: 代码已经复制到剪贴板
2. 浏览器缓存
为了提高用户加载页面的速度,对静态资源进行缓存是非常必要的,根据是否需要重新向服务器发起请求来分类,将 HTTP 缓存规则分为两大类(强制缓存,对比缓存)
3. CDN 的使用
浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限,如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率
4. 使用 Chrome Performance 查找性能瓶颈
Chrome 的 Performance 面板可以录制一段时间内的 js 执行细节及时间。使用 Chrome 开发者工具分析页面性能的步骤如下。
- 打开
Chrome开发者工具,切换到Performance面板 - 点击
Record开始录制 - 刷新页面或展开某个节点
- 点击
Stop停止录制

# 55 Vue与Angular以及React的区别?
# Vue与AngularJS的区别
Angular采用TypeScript开发, 而Vue可以使用javascript也可以使用TypeScriptAngularJS依赖对数据做脏检查,所以Watcher越多越慢;Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。AngularJS社区完善,Vue的学习成本较小
# Vue与React的区别
相同点:
Virtual DOM。其中最大的一个相似之处就是都使用了Virtual DOM。(当然Vue是在Vue2.x才引用的)也就是能让我们通过操作数据的方式来改变真实的DOM状态。因为其实Virtual DOM的本质就是一个JS对象,它保存了对真实DOM的所有描述,是真实DOM的一个映射,所以当我们在进行频繁更新元素的时候,改变这个JS对象的开销远比直接改变真实DOM要小得多。- 组件化的开发思想。第二点来说就是它们都提倡这种组件化的开发思想,也就是建议将应用分拆成一个个功能明确的模块,再将这些模块整合在一起以满足我们的业务需求。
Props。Vue和React中都有props的概念,允许父组件向子组件传递数据。- 构建工具、Chrome插件、配套框架。还有就是它们的构建工具以及Chrome插件、配套框架都很完善。比如构建工具,
React中可以使用CRA,Vue中可以使用对应的脚手架vue-cli。对于配套框架Vue中有vuex、vue-router,React中有react-router、redux。
不同点
- 模版的编写。最大的不同就是模版的编写,
Vue鼓励你去写近似常规HTML的模板,React推荐你使用JSX去书写。 - 状态管理与对象属性。在
React中,应用的状态是比较关键的概念,也就是state对象,它允许你使用setState去更新状态。但是在Vue中,state对象并不是必须的,数据是由data属性在Vue对象中进行管理。 - 虚拟
DOM的处理方式不同。Vue中的虚拟DOM控制了颗粒度,组件层面走watcher通知,而组件内部走vdom做diff,这样,既不会有太多watcher,也不会让vdom的规模过大。而React走了类似于CPU调度的逻辑,把vdom这棵树,微观上变成了链表,然后利用浏览器的空闲时间来做diff